├── .github
└── workflows
│ ├── android.yml
│ └── ios.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── composeApp
├── build.gradle.kts
└── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ ├── actual.kt
│ │ └── dev
│ │ │ └── johnoreilly
│ │ │ └── gemini
│ │ │ ├── AndroidJsonDatabase.kt
│ │ │ ├── AndroidTextToSpeech.kt
│ │ │ └── MainActivity.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ └── strings.xml
│ ├── commonMain
│ ├── composeResources
│ │ └── drawable
│ │ │ ├── assistant.png
│ │ │ ├── chat.png
│ │ │ ├── copy.png
│ │ │ ├── dust.png
│ │ │ ├── moon.png
│ │ │ ├── rotate_right.xml
│ │ │ ├── sound.png
│ │ │ └── sun.png
│ └── kotlin
│ │ ├── App.kt
│ │ ├── GeminiApi.kt
│ │ ├── core
│ │ ├── app_db
│ │ │ ├── ChatDbManager.kt
│ │ │ └── DataStoreManager.kt
│ │ └── models
│ │ │ └── ChatMessage.kt
│ │ ├── expect.kt
│ │ ├── ui
│ │ ├── screens
│ │ │ └── gemini_ai
│ │ │ │ ├── Assistant.kt
│ │ │ │ ├── Chat.kt
│ │ │ │ ├── GeminiAIScreen.kt
│ │ │ │ └── GeminiAiScreenModel.kt
│ │ ├── theme
│ │ │ └── app_theme.kt
│ │ └── widgets.kt
│ │ └── utils
│ │ ├── AppConstants.kt
│ │ └── ConnectionState.kt
│ ├── desktopMain
│ └── kotlin
│ │ ├── DeskTopJsonDB.kt
│ │ ├── DesktopTextToSpeech.kt
│ │ ├── actual.kt
│ │ └── main.kt
│ ├── iosMain
│ └── kotlin
│ │ ├── IosJsonDB.kt
│ │ ├── IosTextToSpeech.kt
│ │ ├── MainViewController.kt
│ │ └── actual.kt
│ └── wasmJsMain
│ ├── kotlin
│ ├── WebJsonDB.kt
│ ├── WebTextToSpeech.kt
│ ├── actual.kt
│ └── main.kt
│ └── resources
│ └── index.html
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── iosApp
├── Configuration
│ └── Config.xcconfig
├── iosApp.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcuserdata
│ │ │ └── joreilly.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata
│ │ └── joreilly.xcuserdatad
│ │ └── xcschemes
│ │ ├── iosApp.xcscheme
│ │ └── xcschememanagement.plist
└── iosApp
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ └── app-icon-1024.png
│ └── Contents.json
│ ├── ContentView.swift
│ ├── Info.plist
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── iOSApp.swift
├── settings.gradle.kts
└── wearApp
├── build.gradle.kts
└── src
└── main
├── AndroidManifest.xml
├── kotlin
└── dev
│ └── johnoreilly
│ └── gemini
│ ├── common
│ └── GeminiApi.kt
│ └── wear
│ ├── MainActivity.kt
│ ├── Screen.kt
│ ├── WearApp.kt
│ ├── markdown
│ └── WearMaterialTypography.kt
│ └── prompt
│ ├── GeminiPromptScreen.kt
│ └── GeminiPromptViewModel.kt
└── res
├── drawable-v24
└── ic_launcher_foreground.xml
├── drawable
└── ic_launcher_background.xml
├── mipmap-anydpi-v26
├── ic_launcher.xml
└── ic_launcher_round.xml
├── mipmap-hdpi
├── ic_launcher.png
└── ic_launcher_round.png
├── mipmap-mdpi
├── ic_launcher.png
└── ic_launcher_round.png
├── mipmap-xhdpi
├── ic_launcher.png
└── ic_launcher_round.png
├── mipmap-xxhdpi
├── ic_launcher.png
└── ic_launcher_round.png
├── mipmap-xxxhdpi
├── ic_launcher.png
└── ic_launcher_round.png
└── values
└── strings.xml
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on: pull_request
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: macos-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: set up JDK 17
13 | uses: actions/setup-java@v4
14 | with:
15 | distribution: 'zulu'
16 | java-version: 17
17 | - name: Build android app
18 | run: ./gradlew assembleDebug
19 | - name: Run Unit Tests
20 | run: ./gradlew test
21 | - name: Build iOS shared code
22 | run: ./gradlew :composeApp:compileKotlinIosSimulatorArm64
23 |
24 |
--------------------------------------------------------------------------------
/.github/workflows/ios.yml:
--------------------------------------------------------------------------------
1 | name: iOS CI
2 |
3 | on: pull_request
4 |
5 | # Cancel any current or previous job from the same PR
6 | concurrency:
7 | group: ios-${{ github.head_ref }}
8 | cancel-in-progress: true
9 |
10 |
11 | jobs:
12 | build:
13 | runs-on: macos-14
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: actions/setup-java@v4
17 | with:
18 | distribution: 'zulu'
19 | java-version: 17
20 |
21 | - name: Build iOS app
22 | run: xcodebuild -allowProvisioningUpdates -workspace iosApp/iosApp.xcodeproj/project.xcworkspace -configuration Debug -scheme iosApp -sdk iphoneos -destination name='iPhone 15'
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | /local.properties
8 | **/.DS_Store
9 | **/build/
10 | .gradle
11 | **/.idea/*
12 | .kotlin
13 | yarn.lock
14 | *.xcworkspacedata
15 |
16 | # Created by https://www.toptal.com/developers/gitignore/api/kotlin,database,androidstudio,intellij+all,java
17 | # Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,database,androidstudio,intellij+all,java
18 |
19 | ### Database ###
20 | *.accdb
21 | *.db
22 | *.dbf
23 | *.mdb
24 | *.pdb
25 | *.sqlite3
26 | *.db-shm
27 | *.db-wal
28 |
29 | ### Intellij+all ###
30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
31 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
32 |
33 | # User-specific stuff
34 | .idea/**/workspace.xml
35 | .idea/**/tasks.xml
36 | .idea/**/usage.statistics.xml
37 | .idea/**/dictionaries
38 | .idea/**/shelf
39 |
40 | # AWS User-specific
41 | .idea/**/aws.xml
42 |
43 | # Generated files
44 | .idea/**/contentModel.xml
45 |
46 | # Sensitive or high-churn files
47 | .idea/**/dataSources/
48 | .idea/**/dataSources.ids
49 | .idea/**/dataSources.local.xml
50 | .idea/**/sqlDataSources.xml
51 | .idea/**/dynamic.xml
52 | .idea/**/uiDesigner.xml
53 | .idea/**/dbnavigator.xml
54 |
55 | # Gradle
56 | .idea/**/gradle.xml
57 | .idea/**/libraries
58 |
59 | # Gradle and Maven with auto-import
60 | # When using Gradle or Maven with auto-import, you should exclude module files,
61 | # since they will be recreated, and may cause churn. Uncomment if using
62 | # auto-import.
63 | # .idea/artifacts
64 | # .idea/compiler.xml
65 | # .idea/jarRepositories.xml
66 | # .idea/modules.xml
67 | # .idea/*.iml
68 | # .idea/modules
69 | # *.iml
70 | # *.ipr
71 |
72 | # CMake
73 | cmake-build-*/
74 |
75 | # Mongo Explorer plugin
76 | .idea/**/mongoSettings.xml
77 |
78 | # File-based project format
79 | *.iws
80 |
81 | # IntelliJ
82 | out/
83 |
84 | # mpeltonen/sbt-idea plugin
85 | .idea_modules/
86 |
87 | # JIRA plugin
88 | atlassian-ide-plugin.xml
89 |
90 | # Cursive Clojure plugin
91 | .idea/replstate.xml
92 |
93 | # SonarLint plugin
94 | .idea/sonarlint/
95 |
96 | # Crashlytics plugin (for Android Studio and IntelliJ)
97 | com_crashlytics_export_strings.xml
98 | crashlytics.properties
99 | crashlytics-build.properties
100 | fabric.properties
101 |
102 | # Editor-based Rest Client
103 | .idea/httpRequests
104 |
105 | # Android studio 3.1+ serialized cache file
106 | .idea/caches/build_file_checksums.ser
107 |
108 | ### Intellij+all Patch ###
109 | # Ignore everything but code style settings and run configurations
110 | # that are supposed to be shared within teams.
111 |
112 | .idea/*
113 |
114 | !.idea/codeStyles
115 | !.idea/runConfigurations
116 |
117 | ### Java ###
118 | # Compiled class file
119 | *.class
120 |
121 | # Log file
122 | *.log
123 |
124 | # BlueJ files
125 | *.ctxt
126 |
127 | # Mobile Tools for Java (J2ME)
128 | .mtj.tmp/
129 |
130 | # Package Files #
131 | *.jar
132 | *.war
133 | *.nar
134 | *.ear
135 | *.zip
136 | *.tar.gz
137 | *.rar
138 |
139 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
140 | hs_err_pid*
141 | replay_pid*
142 |
143 | ### Kotlin ###
144 | # Compiled class file
145 |
146 | # Log file
147 |
148 | # BlueJ files
149 |
150 | # Mobile Tools for Java (J2ME)
151 |
152 | # Package Files #
153 |
154 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
155 |
156 | ### AndroidStudio ###
157 | # Covers files to be ignored for android development using Android Studio.
158 |
159 | # Built application files
160 | *.apk
161 | *.ap_
162 | *.aab
163 |
164 | # Files for the ART/Dalvik VM
165 | *.dex
166 |
167 | # Java class files
168 |
169 | # Generated files
170 | bin/
171 | gen/
172 |
173 | # Gradle files
174 | .gradle
175 | .gradle/
176 | build/
177 |
178 | # Signing files
179 | .signing/
180 |
181 | # Local configuration file (sdk path, etc)
182 | local.properties
183 |
184 | # Proguard folder generated by Eclipse
185 | proguard/
186 |
187 | # Log Files
188 |
189 | # Android Studio
190 | /*/build/
191 | /*/local.properties
192 | /*/out
193 | /*/*/build
194 | /*/*/production
195 | captures/
196 | .navigation/
197 | *.ipr
198 | *~
199 | *.swp
200 |
201 | # Keystore files
202 | *.jks
203 | *.keystore
204 |
205 | # Google Services (e.g. APIs or Firebase)
206 | # google-services.json
207 |
208 | # Android Patch
209 | gen-external-apklibs
210 |
211 | # External native build folder generated in Android Studio 2.2 and later
212 | .externalNativeBuild
213 |
214 | # NDK
215 | obj/
216 |
217 | # IntelliJ IDEA
218 | *.iml
219 | /out/
220 |
221 | # User-specific configurations
222 | .idea/caches/
223 | .idea/libraries/
224 | .idea/shelf/
225 | .idea/workspace.xml
226 | .idea/tasks.xml
227 | .idea/.name
228 | .idea/compiler.xml
229 | .idea/copyright/profiles_settings.xml
230 | .idea/encodings.xml
231 | .idea/misc.xml
232 | .idea/modules.xml
233 | .idea/scopes/scope_settings.xml
234 | .idea/dictionaries
235 | .idea/vcs.xml
236 | .idea/jsLibraryMappings.xml
237 | .idea/datasources.xml
238 | .idea/dataSources.ids
239 | .idea/sqlDataSources.xml
240 | .idea/dynamic.xml
241 | .idea/uiDesigner.xml
242 | .idea/assetWizardSettings.xml
243 | .idea/gradle.xml
244 | .idea/jarRepositories.xml
245 | .idea/navEditor.xml
246 |
247 | # Legacy Eclipse project files
248 | .classpath
249 | .project
250 | .cproject
251 | .settings/
252 |
253 | # Mobile Tools for Java (J2ME)
254 |
255 | # Package Files #
256 |
257 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
258 |
259 | ## Plugin-specific files:
260 |
261 | # mpeltonen/sbt-idea plugin
262 |
263 | # JIRA plugin
264 |
265 | # Mongo Explorer plugin
266 | .idea/mongoSettings.xml
267 |
268 | # Crashlytics plugin (for Android Studio and IntelliJ)
269 |
270 | ### AndroidStudio Patch ###
271 |
272 | !/gradle/wrapper/gradle-wrapper.jar
273 |
274 | # End of https://www.toptal.com/developers/gitignore/api/kotlin,database,androidstudio,intellij+all,java
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Kotlin/Compose Multiplatform sample to demonstrate Gemini Generative AI APIs (text and image based queries).
4 | Uses [Generative AI SDK](https://github.com/PatilShreyas/generative-ai-kmp).
5 |
6 |
7 | Running on
8 | * iOS
9 | * Android
10 | * Wear OS (contributed by https://github.com/yschimke)
11 | * Desktop
12 | * Web (Wasm)
13 |
14 | Set your Gemini API key (`gemini_api_key`) in `local.properties`
15 |
16 | Related posts:
17 | * [Exploring use of Gemini Generative AI APIs in a Kotlin/Compose Multiplatform project](https://johnoreilly.dev/posts/gemini-kotlin-multiplatform/)
18 |
19 |
20 |
21 | ## Screenshots
22 |
23 | ### iOS
24 |
25 | 
26 |
27 |
28 | ### Android
29 |
30 |
31 | 
32 |
33 |
34 | ### Compose for Desktop
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | ### Wasm based Compose for Web
46 |
47 |
48 |
49 |
50 |
51 | ## Full set of Kotlin Multiplatform/Compose/SwiftUI samples
52 |
53 | * PeopleInSpace (https://github.com/joreilly/PeopleInSpace)
54 | * GalwayBus (https://github.com/joreilly/GalwayBus)
55 | * Confetti (https://github.com/joreilly/Confetti)
56 | * BikeShare (https://github.com/joreilly/BikeShare)
57 | * FantasyPremierLeague (https://github.com/joreilly/FantasyPremierLeague)
58 | * ClimateTrace (https://github.com/joreilly/ClimateTraceKMP)
59 | * GeminiKMP (https://github.com/joreilly/GeminiKMP)
60 | * MortyComposeKMM (https://github.com/joreilly/MortyComposeKMM)
61 | * StarWars (https://github.com/joreilly/StarWars)
62 | * WordMasterKMP (https://github.com/joreilly/WordMasterKMP)
63 | * Chip-8 (https://github.com/joreilly/chip-8)
64 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidApplication) apply false
3 | alias(libs.plugins.androidLibrary) apply false
4 | alias(libs.plugins.jetbrainsCompose) apply false
5 | alias(libs.plugins.kotlinMultiplatform) apply false
6 | }
--------------------------------------------------------------------------------
/composeApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.codingfeline.buildkonfig.compiler.FieldSpec
2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
3 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
4 | import java.util.Properties
5 |
6 | plugins {
7 | alias(libs.plugins.kotlinMultiplatform)
8 | alias(libs.plugins.androidApplication)
9 | alias(libs.plugins.jetbrainsCompose)
10 | alias(libs.plugins.compose.compiler)
11 | alias(libs.plugins.kotlinx.serialization)
12 | alias(libs.plugins.buildkonfig)
13 | }
14 |
15 | kotlin {
16 |
17 | androidTarget {
18 | compilations.all {
19 | kotlinOptions {
20 | jvmTarget = "1.8"
21 | }
22 | }
23 | }
24 |
25 | jvm("desktop")
26 |
27 | @OptIn(ExperimentalWasmDsl::class, org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
28 | wasmJs {
29 | moduleName = "composeApp"
30 | browser {
31 | commonWebpackConfig {
32 | outputFileName = "composeApp.js"
33 | }
34 | }
35 | binaries.executable()
36 | }
37 |
38 | listOf(
39 | iosX64(),
40 | iosArm64(),
41 | iosSimulatorArm64()
42 | ).forEach { iosTarget ->
43 | iosTarget.binaries.framework {
44 | baseName = "ComposeApp"
45 | isStatic = true
46 | }
47 | }
48 |
49 | sourceSets {
50 |
51 | all {
52 | languageSettings {
53 | optIn("nl.marc_apps.tts.experimental.ExperimentalVoiceApi")
54 | optIn("nl.marc_apps.tts.experimental.ExperimentalDesktopTarget")
55 | }
56 | }
57 |
58 | commonMain.dependencies {
59 | implementation(compose.runtime)
60 | implementation(compose.foundation)
61 | implementation(compose.material3)
62 | implementation(compose.ui)
63 | implementation(compose.components.resources)
64 | implementation(compose.components.uiToolingPreview)
65 |
66 | implementation(libs.kotlinx.coroutines.core)
67 |
68 | implementation(libs.markdown.renderer)
69 | api(libs.compose.window.size)
70 |
71 | api(libs.generativeai)
72 |
73 | implementation(libs.filekit.dialogs.compose)
74 | implementation(libs.filekit.coil)
75 | // voyager is a multiplatform library for viewmodel navigation
76 | implementation(libs.voyager.navigator)
77 | implementation(libs.voyager.screenmodel)
78 | // storage data
79 | implementation(libs.multiplatform.settings)
80 | implementation(libs.multiplatform.settings.coroutines)
81 | // Time
82 | implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
83 | implementation(libs.kotlinx.serialization.json)
84 |
85 | }
86 | androidMain.dependencies {
87 | implementation(libs.compose.ui.tooling.preview)
88 | implementation(libs.androidx.activity.compose)
89 | implementation(libs.kotlinx.coroutines.android)
90 | }
91 |
92 | val desktopMain by getting {
93 | dependencies {
94 | implementation(compose.desktop.currentOs)
95 | implementation(libs.kotlinx.coroutines.swing)
96 | implementation("nl.marc-apps:tts:2.5.0")
97 | }
98 | }
99 |
100 | iosMain.dependencies {}
101 |
102 | wasmJsMain.dependencies {
103 | implementation("nl.marc-apps:tts:2.5.0")
104 | }
105 |
106 | }
107 | }
108 |
109 | android {
110 | namespace = "dev.johnoreilly.gemini"
111 | compileSdk = 35
112 |
113 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
114 | sourceSets["main"].res.srcDirs("src/androidMain/res")
115 | sourceSets["main"].resources.srcDirs("src/commonMain/resources")
116 |
117 | defaultConfig {
118 | applicationId = "dev.johnoreilly.gemini"
119 | minSdk = libs.versions.android.minSdk.get().toInt()
120 | targetSdk = libs.versions.android.targetSdk.get().toInt()
121 | versionCode = 1
122 | versionName = "1.0"
123 | }
124 | packaging {
125 | resources {
126 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
127 | }
128 | }
129 | buildTypes {
130 | getByName("release") {
131 | isMinifyEnabled = false
132 | }
133 | }
134 | compileOptions {
135 | sourceCompatibility = JavaVersion.VERSION_1_8
136 | targetCompatibility = JavaVersion.VERSION_1_8
137 | }
138 | dependencies {
139 | debugImplementation(libs.compose.ui.tooling)
140 | }
141 | }
142 |
143 | compose.desktop {
144 | application {
145 | mainClass = "MainKt"
146 |
147 | nativeDistributions {
148 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
149 | packageName = "dev.johnoreilly.gemini"
150 | packageVersion = "1.0.0"
151 | }
152 | }
153 | }
154 |
155 | buildkonfig {
156 | packageName = "dev.johnoreilly.gemini"
157 |
158 | val localPropsFile = rootProject.file("local.properties")
159 | val localProperties = Properties()
160 | if (localPropsFile.exists()) {
161 | runCatching {
162 | localProperties.load(localPropsFile.inputStream())
163 | }.getOrElse {
164 | it.printStackTrace()
165 | }
166 | }
167 | defaultConfigs {
168 | buildConfigField(
169 | FieldSpec.Type.STRING,
170 | "GEMINI_API_KEY",
171 | localProperties["gemini_api_key"]?.toString() ?: ""
172 | )
173 | }
174 |
175 | }
176 |
177 | compose.experimental {
178 | web.application {}
179 | }
180 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/actual.kt:
--------------------------------------------------------------------------------
1 | import android.content.Context
2 | import android.graphics.BitmapFactory
3 | import android.os.Build
4 | import androidx.compose.ui.graphics.ImageBitmap
5 | import androidx.compose.ui.graphics.asImageBitmap
6 | import com.russhwolf.settings.ObservableSettings
7 | import com.russhwolf.settings.Settings
8 | import com.russhwolf.settings.SharedPreferencesSettings
9 | import dev.johnoreilly.gemini.AndroidJsonDatabase
10 | import dev.johnoreilly.gemini.AndroidTextToSpeech
11 | import dev.johnoreilly.gemini.MainActivity
12 |
13 | actual fun getPlatform(): Platform {
14 | return Platform.Android("Android ${Build.VERSION.SDK_INT}")
15 | }
16 |
17 | actual fun getDataSettings(): Settings {
18 | val sharedPreferences =
19 | MainActivity.instance.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
20 | return SharedPreferencesSettings(sharedPreferences)
21 | }
22 |
23 | actual fun getDataSettingsFlow(): ObservableSettings? {
24 | val sharedPreferences =
25 | MainActivity.instance.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
26 | return SharedPreferencesSettings(sharedPreferences)
27 | }
28 |
29 |
30 | actual fun showAlert(message: String) {
31 | android.widget.Toast.makeText(
32 | MainActivity.instance,
33 | message,
34 | android.widget.Toast.LENGTH_SHORT
35 | ).show()
36 | }
37 |
38 |
39 | actual fun getJsonDatabase(): JsonDatabase = AndroidJsonDatabase()
40 |
41 | actual fun ByteArray.toComposeImageBitmap(): ImageBitmap {
42 | val bitmap = BitmapFactory.decodeByteArray(this, 0, this.size)
43 | return bitmap.asImageBitmap()
44 | }
45 |
46 | actual fun getTextToSpeech(): TextToSpeech = AndroidTextToSpeech()
47 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/dev/johnoreilly/gemini/AndroidJsonDatabase.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini
2 |
3 | import JsonDatabase
4 | import ListString
5 | import android.content.Context
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.flow
8 | import kotlinx.serialization.json.Json
9 | import java.io.File
10 |
11 | class AndroidJsonDatabase : JsonDatabase {
12 | override fun createData(tableName: String, data: ListString): Boolean {
13 | return try {
14 | val jsonString = Json.encodeToString(data)
15 | MainActivity.instance.openFileOutput(tableName, Context.MODE_PRIVATE).use {
16 | it.write(jsonString.toByteArray())
17 | }
18 | true
19 | } catch (e: Exception) {
20 | e.printStackTrace()
21 | false
22 | }
23 | }
24 |
25 |
26 | override fun getData(tableName: String): ListString {
27 | return try {
28 | val file = File(MainActivity.instance.filesDir, tableName)
29 | if (!file.exists()) return "[]"
30 | Json.decodeFromString(file.readText())
31 | } catch (e: Exception) {
32 | e.printStackTrace()
33 | "[]"
34 | }
35 | }
36 |
37 | override fun getDataFlow(tableName: String): Flow {
38 | return flow {
39 | val json = getData(tableName)
40 | emit(json)
41 | }
42 | }
43 |
44 | override fun deleteData(tableName: String): Boolean {
45 | return try {
46 | val file = File(MainActivity.instance.filesDir, tableName)
47 | file.delete()
48 | } catch (e: Exception) {
49 | e.printStackTrace()
50 | false
51 | }
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/dev/johnoreilly/gemini/AndroidTextToSpeech.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini
2 |
3 | import TextToSpeech
4 | import java.util.Locale
5 |
6 | class AndroidTextToSpeech : TextToSpeech {
7 | private val tts = android.speech.tts.TextToSpeech(MainActivity.instance, null)
8 | init {
9 | tts.language = Locale.US
10 | }
11 | override suspend fun speak(text: String) {
12 | tts.speak(text, android.speech.tts.TextToSpeech.QUEUE_FLUSH, null, null)
13 | }
14 |
15 | override suspend fun stop() {
16 | tts.stop()
17 | }
18 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/dev/johnoreilly/gemini/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini
2 |
3 | import App
4 | import android.os.Bundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.tooling.preview.Preview
9 |
10 | class MainActivity : ComponentActivity() {
11 | companion object{
12 | lateinit var instance: MainActivity
13 | }
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | instance = this
18 | setContent {
19 | App()
20 | }
21 | }
22 |
23 | }
24 |
25 | @Preview
26 | @Composable
27 | fun AppAndroidPreview() {
28 | App()
29 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | GeminiKMP
3 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/assistant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/assistant.png
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/chat.png
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/copy.png
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/dust.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/dust.png
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/moon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/moon.png
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/rotate_right.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/sound.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/sound.png
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/sun.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/sun.png
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/App.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.Composable
2 | import androidx.compose.runtime.collectAsState
3 | import androidx.compose.runtime.getValue
4 | import core.app_db.DataManager
5 | import cafe.adriel.voyager.navigator.Navigator
6 | import org.jetbrains.compose.ui.tooling.preview.Preview
7 | import ui.screens.gemini_ai.GeminiAIScreen
8 | import ui.theme.AppTheme
9 |
10 | @Preview
11 | @Composable
12 | fun App() {
13 | val theme by DataManager.getValueFlow(DataManager.THEME_KEY)
14 | .collectAsState(initial = null)
15 | AppTheme(
16 | mode = when (theme?.lowercase()) {
17 | "dark" -> AppTheme.Dark
18 | "light" -> AppTheme.Light
19 | else -> AppTheme.System
20 | }
21 | ) {
22 | Navigator(GeminiAIScreen)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/GeminiApi.kt:
--------------------------------------------------------------------------------
1 | import core.models.ChatMessage
2 | import dev.johnoreilly.gemini.BuildKonfig
3 | import dev.shreyaspatil.ai.client.generativeai.Chat
4 | import dev.shreyaspatil.ai.client.generativeai.GenerativeModel
5 | import dev.shreyaspatil.ai.client.generativeai.type.Content
6 | import dev.shreyaspatil.ai.client.generativeai.type.GenerateContentResponse
7 | import dev.shreyaspatil.ai.client.generativeai.type.PlatformImage
8 | import dev.shreyaspatil.ai.client.generativeai.type.content
9 | import kotlinx.coroutines.flow.Flow
10 |
11 |
12 | class GeminiApi {
13 | companion object {
14 | const val PROMPT_GENERATE_UI = "Act as an Android app developer. " +
15 | "For the image provided, use Jetpack Compose to build the screen so that " +
16 | "the Compose Preview is as close to this image as possible. Also make sure " +
17 | "to include imports and use Material3. Only give code part without any extra " +
18 | "text or description neither at start or end, your response should contain " +
19 | "only code without any explanation."
20 | }
21 |
22 |
23 | private val apiKey = BuildKonfig.GEMINI_API_KEY
24 |
25 |
26 | private val generativeVisionModel = GenerativeModel(
27 | modelName = "gemini-1.5-flash",
28 | apiKey = apiKey
29 | )
30 |
31 | private val generativeModel = GenerativeModel(
32 | // modelName = "gemini-pro",
33 | modelName = "gemini-2.0-flash", // use this if you are having issues with gemini-pro
34 | apiKey = apiKey
35 | )
36 |
37 | fun generateContent(prompt: String): Flow {
38 | return generativeModel.generateContentStream(prompt)
39 | }
40 |
41 | fun generateContent(prompt: String, imageData: ByteArray): Flow {
42 | val content = content {
43 | image(PlatformImage(imageData))
44 | text(prompt)
45 | }
46 | return generativeVisionModel.generateContentStream(content)
47 | }
48 |
49 | fun generateChat(prompt: List): Chat {
50 | val history = mutableListOf()
51 | prompt.forEach { p ->
52 | if (p.sender.lowercase() == "user") {
53 | history.add(content("user") { text(p.message) })
54 | } else {
55 | history.add(content("assistant") { text(p.message) })
56 | }
57 | }
58 | return generativeModel.startChat(history)
59 | }
60 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/core/app_db/ChatDbManager.kt:
--------------------------------------------------------------------------------
1 | package core.app_db
2 |
3 | import JsonDatabase
4 | import core.models.ChatMessage
5 | import getJsonDatabase
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 | import kotlinx.serialization.json.Json
9 |
10 | object ChatDbManager {
11 |
12 | private val jsonDatabase: JsonDatabase = getJsonDatabase()
13 | private const val CHAT_TABLE = "chat.json"
14 |
15 |
16 | suspend fun insertObjectToStore(chatMessage: ChatMessage) {
17 | withContext(Dispatchers.Main) {
18 | val data = jsonDatabase.getData(CHAT_TABLE)
19 | val jdata = Json.decodeFromString>(data)
20 | val jData = jdata.toMutableList()
21 | jData.add(chatMessage)
22 | jsonDatabase.createData(CHAT_TABLE, Json.encodeToString(jData))
23 | }
24 | }
25 |
26 | suspend fun getObjectToStores(): List {
27 | return withContext(Dispatchers.Main) {
28 | val data = jsonDatabase.getData(CHAT_TABLE)
29 | if (data.isEmpty()) return@withContext emptyList()
30 | Json.decodeFromString>(data).toMutableList()
31 | }
32 | }
33 |
34 | suspend fun deleteAllObjectFromStore() {
35 | withContext(Dispatchers.Main) {
36 | jsonDatabase.deleteData(CHAT_TABLE)
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/core/app_db/DataStoreManager.kt:
--------------------------------------------------------------------------------
1 | package core.app_db
2 |
3 | import com.russhwolf.settings.ExperimentalSettingsApi
4 | import com.russhwolf.settings.coroutines.getStringOrNullFlow
5 | import com.russhwolf.settings.set
6 | import getDataSettings
7 | import getDataSettingsFlow
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flowOf
10 |
11 |
12 | @OptIn(ExperimentalSettingsApi::class)
13 | object DataManager {
14 | private val settings = getDataSettings()
15 | private val settingsFlow = getDataSettingsFlow()
16 | const val THEME_KEY = "theme"
17 |
18 | // Save functions
19 | fun setValue(key: String, value: String) = settings.set(key = key, value = value)
20 |
21 | // Regular get functions
22 | fun getValue(key: String): String = settings.getString(key, "null")
23 |
24 |
25 | fun getValueFlow(key: String): Flow {
26 | return settingsFlow?.getStringOrNullFlow(key = key) // Reactive flow if available
27 | ?: flowOf(getValue(key)) // Emit a single value if no flow is available
28 | }
29 |
30 | fun clear(key: String) {
31 | settings.remove(key)
32 | }
33 |
34 | fun clear() {
35 | settings.clear()
36 | settingsFlow?.clear()
37 | }
38 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/core/models/ChatMessage.kt:
--------------------------------------------------------------------------------
1 | package core.models
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class ChatMessage(
7 | val id: Long? = null,
8 | val message: String,
9 | val sender: String,
10 | val time: String
11 | )
12 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/expect.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.graphics.ImageBitmap
2 | import com.russhwolf.settings.ObservableSettings
3 | import com.russhwolf.settings.Settings
4 | import kotlinx.coroutines.flow.Flow
5 |
6 |
7 | sealed class Platform {
8 | data class Android(val message: String) : Platform()
9 | data class Ios(val message: String) : Platform()
10 | data class Desktop(val message: String) : Platform()
11 | data class Web(val message: String) : Platform()
12 | }
13 |
14 | interface JsonDatabase {
15 | fun createData(tableName: String, data: ListString): Boolean
16 | fun getData(tableName: String): ListString
17 | fun getDataFlow(tableName: String): Flow
18 | fun deleteData(tableName: String): Boolean
19 | }
20 |
21 | interface TextToSpeech{
22 | suspend fun speak(text: String)
23 | suspend fun stop()
24 | }
25 |
26 | expect fun getPlatform(): Platform
27 |
28 | expect fun getJsonDatabase(): JsonDatabase
29 |
30 | expect fun getTextToSpeech(): TextToSpeech
31 |
32 | expect fun ByteArray.toComposeImageBitmap(): ImageBitmap
33 |
34 | expect fun getDataSettings(): Settings
35 |
36 | expect fun getDataSettingsFlow(): ObservableSettings?
37 |
38 | expect fun showAlert(message: String)
39 |
40 | typealias ListString = String
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/screens/gemini_ai/Assistant.kt:
--------------------------------------------------------------------------------
1 | package ui.screens.gemini_ai
2 |
3 | import GeminiApi
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.ExperimentalLayoutApi
8 | import androidx.compose.foundation.layout.FlowRow
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.defaultMinSize
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.height
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.rememberScrollState
16 | import androidx.compose.foundation.text.selection.SelectionContainer
17 | import androidx.compose.foundation.verticalScroll
18 | import androidx.compose.material.icons.Icons
19 | import androidx.compose.material.icons.filled.Clear
20 | import androidx.compose.material3.ButtonDefaults
21 | import androidx.compose.material3.CircularProgressIndicator
22 | import androidx.compose.material3.Icon
23 | import androidx.compose.material3.IconButton
24 | import androidx.compose.material3.MaterialTheme
25 | import androidx.compose.material3.OutlinedButton
26 | import androidx.compose.material3.OutlinedTextField
27 | import androidx.compose.material3.Text
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.runtime.derivedStateOf
30 | import androidx.compose.runtime.getValue
31 | import androidx.compose.runtime.mutableStateOf
32 | import androidx.compose.runtime.remember
33 | import androidx.compose.runtime.rememberCoroutineScope
34 | import androidx.compose.runtime.setValue
35 | import androidx.compose.ui.Alignment
36 | import androidx.compose.ui.Modifier
37 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController
38 | import androidx.compose.ui.text.font.FontWeight
39 | import androidx.compose.ui.unit.dp
40 | import com.mikepenz.markdown.m3.Markdown
41 | import dev.shreyaspatil.ai.client.generativeai.type.GenerateContentResponse
42 | import io.github.vinceglb.filekit.PlatformFile
43 | import io.github.vinceglb.filekit.coil.AsyncImage
44 | import io.github.vinceglb.filekit.dialogs.FileKitType
45 | import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher
46 | import io.github.vinceglb.filekit.readBytes
47 | import kotlinx.coroutines.flow.Flow
48 | import kotlinx.coroutines.flow.onCompletion
49 | import kotlinx.coroutines.flow.onStart
50 | import kotlinx.coroutines.launch
51 |
52 | @OptIn(ExperimentalLayoutApi::class)
53 | @Composable
54 | fun AssistantScreen() {
55 | val api = remember { GeminiApi() }
56 | val coroutineScope = rememberCoroutineScope()
57 | var prompt by remember { mutableStateOf("") }
58 | var content by remember { mutableStateOf("") }
59 | var showProgress by remember { mutableStateOf(false) }
60 | var selectedImage by remember { mutableStateOf(null) }
61 | val keyboardController = LocalSoftwareKeyboardController.current
62 | val canClearPrompt by remember {
63 | derivedStateOf {
64 | prompt.isNotBlank()
65 | }
66 | }
67 |
68 | val imagePickerLauncher = rememberFilePickerLauncher(FileKitType.Image) { image ->
69 | coroutineScope.launch {
70 | selectedImage = image
71 | }
72 | }
73 |
74 | Column(
75 | modifier = Modifier
76 | .verticalScroll(rememberScrollState())
77 | .fillMaxWidth()
78 | ) {
79 | FlowRow(
80 | modifier = Modifier
81 | .fillMaxWidth()
82 | .padding(vertical = 15.dp, horizontal = 15.dp)
83 | ) {
84 | OutlinedTextField(
85 | value = prompt,
86 | onValueChange = { prompt = it },
87 | modifier = Modifier
88 | .fillMaxSize()
89 | .defaultMinSize(minHeight = 52.dp),
90 | label = {
91 | Text("Search")
92 | },
93 | trailingIcon = {
94 | if (canClearPrompt) {
95 | IconButton(
96 | onClick = { prompt = "" }
97 | ) {
98 | Icon(
99 | imageVector = Icons.Default.Clear,
100 | contentDescription = "Clear"
101 | )
102 | }
103 | }
104 | }
105 | )
106 |
107 | OutlinedButton(
108 | colors = ButtonDefaults.outlinedButtonColors(
109 | containerColor = MaterialTheme.colorScheme.secondary,
110 | contentColor = MaterialTheme.colorScheme.onSecondary
111 | ),
112 | onClick = {
113 | if (prompt.isNotBlank()) {
114 | keyboardController?.hide()
115 | coroutineScope.launch {
116 | println("prompt = $prompt")
117 | content = ""
118 | generateContentAsFlow(api, prompt, selectedImage?.readBytes())
119 | .onStart { showProgress = true }
120 | .onCompletion { showProgress = false }
121 | .collect {
122 | println("response = ${it.text}")
123 | content += it.text
124 | }
125 | }
126 | }
127 | },
128 | enabled = prompt.isNotBlank(),
129 | modifier = Modifier
130 | .padding(all = 4.dp)
131 | .weight(1f)
132 | .align(Alignment.CenterVertically)
133 | ) {
134 | Text(
135 | "Submit",
136 | style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold)
137 | )
138 | }
139 |
140 | OutlinedButton(
141 | colors = ButtonDefaults.outlinedButtonColors(
142 | containerColor = MaterialTheme.colorScheme.secondary,
143 | contentColor = MaterialTheme.colorScheme.onSecondary
144 | ),
145 | onClick = { imagePickerLauncher.launch() },
146 | modifier = Modifier
147 | .padding(all = 4.dp)
148 | .weight(1f)
149 | .align(Alignment.CenterVertically)
150 | ) {
151 | Text(
152 | "Select Image",
153 | style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold)
154 | )
155 | }
156 |
157 | OutlinedButton(
158 | colors = ButtonDefaults.outlinedButtonColors(
159 | containerColor = MaterialTheme.colorScheme.secondary,
160 | contentColor = MaterialTheme.colorScheme.onSecondary
161 | ),
162 | onClick = {
163 | prompt = GeminiApi.PROMPT_GENERATE_UI
164 | coroutineScope.launch {
165 | content = ""
166 | generateContentAsFlow(api, prompt, selectedImage?.readBytes())
167 | .onStart { showProgress = true }
168 | .onCompletion { showProgress = false }
169 | .collect {
170 | println("response = ${it.text}")
171 | content += it.text
172 | }
173 | }
174 |
175 | },
176 | enabled = selectedImage != null,
177 | modifier = Modifier
178 | .fillMaxWidth()
179 | .padding(all = 4.dp)
180 | .align(Alignment.CenterVertically)
181 | ) {
182 | Text(
183 | "Generate Compose UI Code",
184 | style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold)
185 | )
186 | }
187 | }
188 |
189 | Spacer(Modifier.height(16.dp))
190 |
191 | selectedImage?.let {
192 | Column(
193 | verticalArrangement = Arrangement.Center,
194 | horizontalAlignment = Alignment.CenterHorizontally
195 | ) {
196 | AsyncImage(
197 | file = selectedImage,
198 | contentDescription = "search_image",
199 | modifier = Modifier.fillMaxSize()
200 | )
201 | }
202 | }
203 |
204 | Spacer(Modifier.height(16.dp))
205 | if (showProgress) {
206 | Column(
207 | modifier = Modifier.fillMaxSize(),
208 | verticalArrangement = Arrangement.Center,
209 | horizontalAlignment = Alignment.CenterHorizontally
210 | ) {
211 | CircularProgressIndicator()
212 | }
213 | } else {
214 | SelectionContainer {
215 | Markdown(
216 | modifier = Modifier.fillMaxSize().padding(10.dp),
217 | content = content
218 | )
219 | }
220 | }
221 | }
222 | }
223 |
224 | private fun generateContentAsFlow(
225 | api: GeminiApi,
226 | prompt: String,
227 | imageData: ByteArray? = null
228 | ): Flow = imageData?.let { imageByteArray ->
229 | api.generateContent(prompt, imageByteArray)
230 | } ?: run {
231 | api.generateContent(prompt)
232 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/screens/gemini_ai/Chat.kt:
--------------------------------------------------------------------------------
1 | package ui.screens.gemini_ai
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.gestures.Orientation
5 | import androidx.compose.foundation.gestures.draggable
6 | import androidx.compose.foundation.gestures.rememberDraggableState
7 | import androidx.compose.foundation.gestures.scrollBy
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.lazy.LazyColumn
14 | import androidx.compose.foundation.lazy.items
15 | import androidx.compose.foundation.lazy.rememberLazyListState
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.automirrored.filled.Send
18 | import androidx.compose.material3.Icon
19 | import androidx.compose.material3.IconButton
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.OutlinedTextField
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.rememberCoroutineScope
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.platform.LocalClipboardManager
27 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController
28 | import androidx.compose.ui.text.buildAnnotatedString
29 | import androidx.compose.ui.unit.dp
30 | import geminikmp.composeapp.generated.resources.Res
31 | import geminikmp.composeapp.generated.resources.rotate_right
32 | import kotlinx.coroutines.Dispatchers
33 | import kotlinx.coroutines.launch
34 | import org.jetbrains.compose.resources.painterResource
35 | import showAlert
36 | import ui.ChatBubble
37 | import ui.RotatingIcon
38 | import utils.ConnectionState
39 |
40 |
41 | @Composable
42 | fun ChatScreen(viewModel: AiScreenModel) {
43 | val scope = rememberCoroutineScope()
44 | val scrollState = rememberLazyListState()
45 |
46 | Column(
47 | modifier = Modifier.fillMaxSize(),
48 | horizontalAlignment = Alignment.CenterHorizontally
49 | ) {
50 | val keyboardController = LocalSoftwareKeyboardController.current
51 | LazyColumn(
52 | state = scrollState,
53 | modifier = Modifier
54 | .weight(1f)
55 | .draggable(
56 | orientation = Orientation.Vertical,
57 | state = rememberDraggableState { delta ->
58 | scope.launch {
59 | scrollState.scrollBy(-delta)
60 | }
61 | },
62 | ),
63 | verticalArrangement = Arrangement.Bottom,
64 | horizontalAlignment = Alignment.CenterHorizontally
65 | ) {
66 | items(viewModel.items) { message ->
67 | val clipboardManager = LocalClipboardManager.current
68 | ChatBubble(Modifier.fillMaxWidth(), chatMessage = message) {
69 | if (it.first.trim().lowercase() == "copy") {
70 | clipboardManager.setText(
71 | annotatedString = buildAnnotatedString {
72 | append(text = it.second)
73 | }
74 | )
75 | showAlert("Copied to clipboard")
76 | } else if (it.first.trim().lowercase() == "speak") {
77 | scope.launch(Dispatchers.Default) {
78 | viewModel.textToSpeech.speak(it.second.replace("*", ""))
79 | }
80 | }
81 | }
82 | }
83 | }
84 | OutlinedTextField(
85 | modifier = Modifier
86 | .fillMaxWidth()
87 | .padding(10.dp),
88 | value = viewModel.prompt,
89 | onValueChange = { viewModel.prompt = it },
90 | trailingIcon = {
91 | IconButton(onClick = {
92 | keyboardController?.hide()
93 | viewModel.sendMessage()
94 | }) {
95 | if (viewModel.isLoading is ConnectionState.Loading) {
96 | RotatingIcon(painterResource(Res.drawable.rotate_right))
97 | } else {
98 | Icon(
99 | Icons.AutoMirrored.Filled.Send,
100 | "Send message"
101 | )
102 | }
103 | }
104 | }
105 | )
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/screens/gemini_ai/GeminiAIScreen.kt:
--------------------------------------------------------------------------------
1 | package ui.screens.gemini_ai
2 |
3 | import Platform
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxHeight
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.size
13 | import androidx.compose.material.icons.Icons
14 | import androidx.compose.material.icons.filled.MoreVert
15 | import androidx.compose.material3.DropdownMenu
16 | import androidx.compose.material3.DropdownMenuItem
17 | import androidx.compose.material3.ExperimentalMaterial3Api
18 | import androidx.compose.material3.Icon
19 | import androidx.compose.material3.IconButton
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.MenuDefaults
22 | import androidx.compose.material3.Scaffold
23 | import androidx.compose.material3.Text
24 | import androidx.compose.material3.TopAppBar
25 | import androidx.compose.material3.TopAppBarDefaults
26 | import androidx.compose.material3.VerticalDivider
27 | import androidx.compose.runtime.Composable
28 | import androidx.compose.runtime.collectAsState
29 | import androidx.compose.runtime.getValue
30 | import androidx.compose.runtime.mutableStateOf
31 | import androidx.compose.runtime.setValue
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.text.font.FontWeight
34 | import androidx.compose.ui.unit.dp
35 | import cafe.adriel.voyager.core.model.rememberScreenModel
36 | import cafe.adriel.voyager.core.screen.Screen
37 | import geminikmp.composeapp.generated.resources.Res
38 | import geminikmp.composeapp.generated.resources.assistant
39 | import geminikmp.composeapp.generated.resources.chat
40 | import geminikmp.composeapp.generated.resources.dust
41 | import geminikmp.composeapp.generated.resources.moon
42 | import geminikmp.composeapp.generated.resources.sun
43 | import getPlatform
44 | import org.jetbrains.compose.resources.painterResource
45 |
46 | object GeminiAIScreen : Screen {
47 | @OptIn(ExperimentalMaterial3Api::class)
48 | @Composable
49 | override fun Content() {
50 | val viewModel = rememberScreenModel { AiScreenModel() }
51 | var expand by mutableStateOf(false)
52 | val theme by viewModel.theme.collectAsState() // For StateFlow
53 |
54 |
55 | Scaffold(
56 | topBar = {
57 | TopAppBar(
58 | colors = TopAppBarDefaults.topAppBarColors(
59 | containerColor = MaterialTheme.colorScheme.primary,
60 | titleContentColor = MaterialTheme.colorScheme.onPrimary,
61 | actionIconContentColor = MaterialTheme.colorScheme.onPrimary
62 | ),
63 | title = {
64 | Text(
65 | "Gemini",
66 | style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold)
67 | )
68 | },
69 | actions = {
70 | IconButton(onClick = { viewModel.updateTheme() }) {
71 | Icon(
72 | modifier = Modifier.size(25.dp),
73 | painter = if (theme?.lowercase() == "dark")
74 | painterResource(Res.drawable.moon)
75 | else
76 | painterResource(Res.drawable.sun),
77 | contentDescription = null
78 | )
79 | }
80 | if (viewModel.screen == AiScreenType.Chat) {
81 | IconButton(onClick = { viewModel.clearDatabase() }) {
82 | Icon(
83 | modifier = Modifier.size(25.dp),
84 | painter = painterResource(Res.drawable.dust),
85 | contentDescription = null
86 | )
87 | }
88 | }
89 | if (getPlatform() is Platform.Android || getPlatform() is Platform.Ios) {
90 | IconButton(onClick = { expand = !expand }) {
91 | Icon(Icons.Default.MoreVert, contentDescription = null)
92 | }
93 | DropdownMenu(
94 | containerColor = MaterialTheme.colorScheme.primary,
95 | expanded = expand,
96 | onDismissRequest = { expand = !expand },
97 | content = {
98 | DropdownMenuItem(
99 | colors = MenuDefaults.itemColors(
100 | textColor = MaterialTheme.colorScheme.onPrimary
101 | ),
102 | enabled = viewModel.screen != AiScreenType.Assistant,
103 | text = {
104 | Text(
105 | "Assistant",
106 | style = MaterialTheme.typography.bodyMedium
107 | )
108 | },
109 | onClick = {
110 | viewModel.changeScreen(AiScreenType.Assistant)
111 | expand = !expand
112 | },
113 | leadingIcon = {
114 | Image(
115 | modifier = Modifier.size(25.dp),
116 | painter = painterResource(Res.drawable.assistant),
117 | contentDescription = "AI Assistant Screen"
118 | )
119 | }
120 | )
121 | DropdownMenuItem(
122 | colors = MenuDefaults.itemColors(
123 | textColor = MaterialTheme.colorScheme.onPrimary
124 | ),
125 | enabled = viewModel.screen != AiScreenType.Chat,
126 | text = {
127 | Text(
128 | "Chat",
129 | style = MaterialTheme.typography.bodyMedium
130 | )
131 | },
132 | onClick = {
133 | viewModel.changeScreen(AiScreenType.Chat)
134 | expand = !expand
135 | },
136 | leadingIcon = {
137 | Image(
138 | modifier = Modifier.size(25.dp),
139 | painter = painterResource(Res.drawable.chat),
140 | contentDescription = "AI Chat screen"
141 | )
142 | }
143 | )
144 | }
145 | )
146 | }
147 | }
148 | )
149 | }
150 | ) { pv ->
151 | Row(Modifier.padding(pv).fillMaxSize()) {
152 | if (getPlatform() is Platform.Desktop || getPlatform() is Platform.Web) {
153 | Column(
154 | Modifier.fillMaxWidth(0.3f).fillMaxHeight(),
155 | verticalArrangement = Arrangement.spacedBy(16.dp)
156 | ) {
157 | DropdownMenuItem(
158 | enabled = viewModel.screen != AiScreenType.Assistant,
159 | text = {
160 | Text(
161 | "Assistant",
162 | style = MaterialTheme.typography.bodyMedium
163 | )
164 | },
165 | onClick = { viewModel.changeScreen(AiScreenType.Assistant) },
166 | leadingIcon = {
167 | Image(
168 | modifier = Modifier.size(50.dp),
169 | painter = painterResource(Res.drawable.assistant),
170 | contentDescription = "AI Assistant Screen"
171 | )
172 | }
173 | )
174 | DropdownMenuItem(
175 | enabled = viewModel.screen != AiScreenType.Chat,
176 | text = { Text("Chat", style = MaterialTheme.typography.bodyMedium) },
177 | onClick = { viewModel.changeScreen(AiScreenType.Chat) },
178 | leadingIcon = {
179 | Image(
180 | modifier = Modifier.size(50.dp),
181 | painter = painterResource(Res.drawable.chat),
182 | contentDescription = "Chat Assistant Screen"
183 | )
184 | }
185 | )
186 | }
187 | VerticalDivider()
188 | }
189 | when (viewModel.screen) {
190 | AiScreenType.Assistant -> AssistantScreen()
191 | AiScreenType.Chat -> ChatScreen(viewModel)
192 | }
193 | }
194 | }
195 | }
196 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/screens/gemini_ai/GeminiAiScreenModel.kt:
--------------------------------------------------------------------------------
1 | package ui.screens.gemini_ai
2 |
3 | import GeminiApi
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import cafe.adriel.voyager.core.model.ScreenModel
8 | import cafe.adriel.voyager.core.model.screenModelScope
9 | import core.app_db.ChatDbManager
10 | import core.app_db.DataManager
11 | import core.models.ChatMessage
12 | import dev.shreyaspatil.ai.client.generativeai.Chat
13 | import dev.shreyaspatil.ai.client.generativeai.type.content
14 | import getTextToSpeech
15 | import kotlinx.coroutines.Dispatchers
16 | import kotlinx.coroutines.flow.SharingStarted
17 | import kotlinx.coroutines.flow.StateFlow
18 | import kotlinx.coroutines.flow.stateIn
19 | import kotlinx.coroutines.launch
20 | import kotlinx.datetime.Clock
21 | import kotlinx.datetime.TimeZone
22 | import kotlinx.datetime.toLocalDateTime
23 | import utils.ConnectionState
24 |
25 | //data class AiMessage(
26 | // val id: Long? = null,
27 | // val aiModel: String,
28 | // val message: String,
29 | // val time: String = ""
30 | //)
31 |
32 | enum class AiScreenType {
33 | Assistant,
34 | Chat
35 | }
36 |
37 |
38 | class AiScreenModel : ScreenModel {
39 | var prompt by mutableStateOf("")
40 | var screen by mutableStateOf(AiScreenType.Assistant)
41 | var isLoading by mutableStateOf(ConnectionState.Default)
42 | val textToSpeech = getTextToSpeech()
43 |
44 | private val geminiApi = GeminiApi()
45 |
46 | // Expose theme as StateFlow for Compose compatibility
47 | val theme: StateFlow = DataManager.getValueFlow(DataManager.THEME_KEY)
48 | .stateIn(
49 | scope = screenModelScope,
50 | started = SharingStarted.WhileSubscribed(5000L),
51 | initialValue = null // Provide a default value or fallback
52 | )
53 | var items by mutableStateOf(emptyList())
54 | private set
55 | private var userChat: Chat by mutableStateOf(geminiApi.generateChat(items))
56 |
57 | init {
58 | screenModelScope.launch(Dispatchers.Main) {
59 | items = ChatDbManager.getObjectToStores()
60 | userChat = geminiApi.generateChat(items)
61 | }
62 | }
63 |
64 | fun sendMessage() {
65 | screenModelScope.launch(Dispatchers.Main) {
66 | isLoading = ConnectionState.Loading
67 | try {
68 | var nowTime = Clock.System.now().toLocalDateTime(TimeZone.UTC)
69 | items = items + ChatMessage(
70 | sender = "user",
71 | message = prompt,
72 | time = "${nowTime.date} // ${nowTime.hour}:${nowTime.minute}:${nowTime.second}"
73 | )
74 | ChatDbManager.insertObjectToStore(items.last())
75 | val result = userChat.sendMessage(content("user") { text(prompt) })
76 | nowTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
77 | items = items + ChatMessage(
78 | sender = "model",
79 | message = result.text ?: "Sorry! could not get a response",
80 | time = "${nowTime.date} // ${nowTime.hour}:${nowTime.minute}:${nowTime.second}"
81 | )
82 | ChatDbManager.insertObjectToStore(items.last())
83 | prompt = ""
84 | isLoading = ConnectionState.Success("Success")
85 | } catch (e: Exception) {
86 | val nowTime = Clock.System.now().toLocalDateTime(TimeZone.UTC)
87 | isLoading = ConnectionState.Error("Could not generate a response")
88 | items = items + ChatMessage(
89 | sender = "model",
90 | message = "Sorry! could not get a response $e",
91 | time = "${nowTime.date} // ${nowTime.hour}:${nowTime.minute}:${nowTime.second}"
92 | )
93 | ChatDbManager.insertObjectToStore(items.last())
94 | println("Error: $e")
95 | prompt = ""
96 | }
97 | }
98 | }
99 |
100 |
101 | // Function to update theme
102 | fun updateTheme() {
103 | screenModelScope.launch(Dispatchers.Main) {
104 | val newTheme = if (theme.value?.lowercase() == "dark") "light" else "dark"
105 | DataManager.setValue(DataManager.THEME_KEY, newTheme)
106 | }
107 | }
108 |
109 | fun clearDatabase() {
110 | screenModelScope.launch(Dispatchers.Main) {
111 | ChatDbManager.deleteAllObjectFromStore()
112 | items = emptyList()
113 | }
114 | }
115 |
116 | fun changeScreen(screen: AiScreenType) {
117 | this.screen = screen
118 | }
119 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/theme/app_theme.kt:
--------------------------------------------------------------------------------
1 | package ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.darkColorScheme
6 | import androidx.compose.material3.lightColorScheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.graphics.Color
10 |
11 | enum class AppTheme {
12 | Light,
13 | Dark,
14 | System
15 | }
16 |
17 | private val lightScheme = lightColorScheme(
18 | primary = Color(10, 99, 0),
19 | onPrimary = Color.White,
20 | secondary = Color(99, 40, 0), // Brown
21 | onSecondary = Color(255, 255, 255), // White (RGB: 255, 255, 255)
22 | background = Color.White,
23 | onBackground = Color.Black,
24 | surface = Color.LightGray,
25 | onSurface = Color(0, 0, 0), // Black (RGB: 0, 0, 0)
26 | error = Color.Red,
27 | onError = Color.White
28 | )
29 |
30 | private val darkScheme = darkColorScheme(
31 | primary = Color(99, 40, 0), // Brown
32 | onPrimary = Color(255, 255, 255), // White (RGB: 255, 255, 255)
33 | secondary = Color(10, 99, 0), // Green
34 | onSecondary = Color.White,
35 | background = Color(0, 0, 0), // Black (RGB: 0, 0, 0)
36 | onBackground = Color(255, 255, 255), // White (RGB: 255, 255, 255)
37 | surface = Color(26, 26, 26), // Dark Gray (RGB: 26, 26, 26)
38 | onSurface = Color(255, 255, 255), // White (RGB: 255, 255, 255)
39 | error = Color(255, 176, 32), // Red (RGB: 255, 176, 32)
40 | onError = Color(0, 0, 0) // Black (RGB: 0, 0, 0)
41 | )
42 |
43 |
44 |
45 | @Composable
46 | fun AppTheme(
47 | mode: AppTheme = AppTheme.System,
48 | content: @Composable () -> Unit,
49 | ) {
50 | val isDark = isSystemInDarkTheme()
51 | val colorScheme = remember(mode) {
52 | when (mode) {
53 | AppTheme.Light -> lightScheme
54 | AppTheme.Dark -> darkScheme
55 | AppTheme.System -> if (isDark) darkScheme else lightScheme
56 | }
57 | }
58 | MaterialTheme(
59 | colorScheme = colorScheme,
60 | typography = MaterialTheme.typography,
61 | content = content
62 | )
63 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/ui/widgets.kt:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import androidx.compose.animation.core.LinearEasing
4 | import androidx.compose.animation.core.RepeatMode
5 | import androidx.compose.animation.core.animateFloat
6 | import androidx.compose.animation.core.infiniteRepeatable
7 | import androidx.compose.animation.core.rememberInfiniteTransition
8 | import androidx.compose.animation.core.tween
9 | import androidx.compose.foundation.Image
10 | import androidx.compose.foundation.background
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Column
13 | import androidx.compose.foundation.layout.Row
14 | import androidx.compose.foundation.layout.Spacer
15 | import androidx.compose.foundation.layout.fillMaxWidth
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.layout.size
18 | import androidx.compose.foundation.layout.width
19 | import androidx.compose.foundation.layout.wrapContentSize
20 | import androidx.compose.material3.Icon
21 | import androidx.compose.material3.IconButton
22 | import androidx.compose.material3.MaterialTheme
23 | import androidx.compose.material3.Text
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.getValue
26 | import androidx.compose.runtime.remember
27 | import androidx.compose.ui.Alignment
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.graphics.graphicsLayer
30 | import androidx.compose.ui.graphics.painter.Painter
31 | import androidx.compose.ui.text.font.FontWeight
32 | import androidx.compose.ui.unit.Dp
33 | import androidx.compose.ui.unit.dp
34 | import com.mikepenz.markdown.m3.Markdown
35 | import core.models.ChatMessage
36 | import geminikmp.composeapp.generated.resources.Res
37 | import geminikmp.composeapp.generated.resources.copy
38 | import geminikmp.composeapp.generated.resources.sound
39 | import org.jetbrains.compose.resources.painterResource
40 |
41 |
42 | @Composable
43 | fun TextIcon(
44 | modifier: Modifier = Modifier,
45 | leadingIcon: @Composable (() -> Unit)? = null,
46 | text: @Composable () -> Unit,
47 | trailingIcon: @Composable (() -> Unit)? = null,
48 | vAlignment: Alignment.Vertical = Alignment.Top,
49 | hArrangement: Arrangement.Horizontal
50 | ) {
51 | Row(modifier, horizontalArrangement = hArrangement, verticalAlignment = vAlignment) {
52 | if (leadingIcon != null) {
53 | leadingIcon()
54 | Spacer(Modifier.width(8.dp))
55 | }
56 | text()
57 | if (trailingIcon != null) {
58 | Spacer(Modifier.width(8.dp))
59 | trailingIcon()
60 | }
61 | }
62 | }
63 |
64 |
65 | @Composable
66 | fun RotatingIcon(
67 | painterIcon: Painter,
68 | modifier: Modifier = Modifier,
69 | sizeDpSize: Dp = 64.dp,
70 | durationMillis: Int = 2000 // Duration of one full rotation
71 | ) {
72 | val infiniteTransition = rememberInfiniteTransition(label = "Rotation")
73 |
74 | val rotationAngle by infiniteTransition.animateFloat(
75 | initialValue = 0f,
76 | targetValue = 360f,
77 | animationSpec = infiniteRepeatable(
78 | animation = tween(
79 | durationMillis = durationMillis,
80 | easing = LinearEasing
81 | ), // Smooth linear rotation
82 | repeatMode = RepeatMode.Restart
83 | ),
84 | label = "Rotation Animation"
85 | )
86 |
87 | Icon(
88 | painter = painterIcon,
89 | contentDescription = "Rotating Icon",
90 | modifier = modifier
91 | .graphicsLayer(rotationZ = rotationAngle) // Apply rotation
92 | .size(sizeDpSize) // Icon size
93 | )
94 | }
95 |
96 |
97 | @Composable
98 | fun ChatBubble(
99 | modifier: Modifier = Modifier,
100 | chatMessage: ChatMessage,
101 | onClick: ((Pair) -> Unit)? = null
102 | ) {
103 | val hArrange =
104 | if (chatMessage.sender.lowercase() == "user") Arrangement.End else Arrangement.Start
105 | val vAlign = if (chatMessage.sender.lowercase() == "user") Alignment.End else Alignment.Start
106 | Row(
107 | modifier = modifier,
108 | horizontalArrangement = hArrange
109 | ) {
110 | Column(
111 | Modifier.fillMaxWidth(0.9f),
112 | horizontalAlignment = vAlign
113 | ) {
114 | Markdown(
115 | modifier = Modifier
116 | .wrapContentSize()
117 | .padding(4.dp)
118 | .background(MaterialTheme.colorScheme.background, MaterialTheme.shapes.small)
119 | .padding(8.dp),
120 | content = chatMessage.message
121 | )
122 | if (onClick != null) {
123 | Row (verticalAlignment = Alignment.CenterVertically){
124 | // speak button icon //
125 | IconButton(
126 | onClick = { onClick(Pair("speak", chatMessage.message)) }
127 | ) {
128 | Image(
129 | painter = painterResource(Res.drawable.sound),
130 | contentDescription = "Speak",
131 | modifier = Modifier.size(24.dp)
132 | )
133 | }
134 | // copy button icon //
135 | IconButton(
136 | onClick = { onClick(Pair("copy", chatMessage.message)) }
137 | ) {
138 | Image(
139 | painter = painterResource(Res.drawable.copy),
140 | contentDescription = "Copy",
141 | modifier = Modifier.size(24.dp)
142 | )
143 | }
144 | Column {
145 | val time = remember { chatMessage.time.split("//") }
146 | Text(
147 | text = time[0],
148 | style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.W300)
149 | )
150 | Text(
151 | text = time[1],
152 | style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.W300)
153 | )
154 | }
155 | }
156 | }
157 | }
158 | }
159 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/utils/AppConstants.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | object AppConstants {
4 |
5 | const val TEST_DATA1 = """
6 | 
7 |
8 | Kotlin/Compose Multiplatform sample to demonstrate Gemini Generative AI APIs (text and image based queries).
9 | Uses [Generative AI SDK](https://github.com/PatilShreyas/generative-ai-kmp).
10 |
11 |
12 | Running on
13 | * iOS
14 | * Android
15 | * Wear OS (contributed by https://github.com/yschimke)
16 | * Desktop
17 | * Web (Wasm)
18 |
19 | Set your Gemini API key (`gemini_api_key`) in `local.properties`
20 |
21 | Related posts:
22 | * [Exploring use of Gemini Generative AI APIs in a Kotlin/Compose Multiplatform project](https://johnoreilly.dev/posts/gemini-kotlin-multiplatform/)
23 |
24 |
25 |
26 | ## Screenshots
27 |
28 | ### iOS
29 |
30 | 
31 |
32 |
33 | ### Android
34 |
35 |
36 | 
37 |
38 |
39 | ### Compose for Desktop
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ### Wasm based Compose for Web
51 |
52 |
53 |
54 |
55 |
56 | ## Full set of Kotlin Multiplatform/Compose/SwiftUI samples
57 |
58 | * PeopleInSpace (https://github.com/joreilly/PeopleInSpace)
59 | * GalwayBus (https://github.com/joreilly/GalwayBus)
60 | * Confetti (https://github.com/joreilly/Confetti)
61 | * BikeShare (https://github.com/joreilly/BikeShare)
62 | * FantasyPremierLeague (https://github.com/joreilly/FantasyPremierLeague)
63 | * ClimateTrace (https://github.com/joreilly/ClimateTraceKMP)
64 | * GeminiKMP (https://github.com/joreilly/GeminiKMP)
65 | * MortyComposeKMM (https://github.com/joreilly/MortyComposeKMM)
66 | * StarWars (https://github.com/joreilly/StarWars)
67 | * WordMasterKMP (https://github.com/joreilly/WordMasterKMP)
68 | * Chip-8 (https://github.com/joreilly/chip-8)
69 | """
70 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/utils/ConnectionState.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | sealed class ConnectionState {
4 | data object Loading : ConnectionState()
5 | data class Error(val errorMessage: String) : ConnectionState()
6 | data class Success(val data: String) : ConnectionState()
7 | data object Default : ConnectionState()
8 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/DeskTopJsonDB.kt:
--------------------------------------------------------------------------------
1 | import kotlinx.coroutines.flow.Flow
2 | import kotlinx.coroutines.flow.flow
3 | import java.io.File
4 |
5 | private const val INFO = """
6 | Gemini KMP folder and its contents can be deleted. (*/.geminikmp/GeminiKmp/*
7 | The .json file is been used by the GeminiKMP app to store chat history. If this folder is deleted
8 | and the app is not installed the app will automatically create this folder.
9 |
10 | from co-developer
11 | (Ohior)
12 | (\__/)
13 | (•ㅅ•) *sniff sniff*
14 | / づ
15 | * *
16 | * 🐭 R A T *
17 | * *
18 | *************
19 | """
20 |
21 | class DeskTopJsonDB : JsonDatabase {
22 | private fun getFilePath(tableName: String): File {
23 | val appDir = File(System.getProperty("user.home"), ".geminikmp/GeminiKMP")
24 | .apply { mkdirs() }
25 | val file = File(appDir.path, tableName)
26 | val infoFile = File(appDir.path, "INFO")
27 | if (!infoFile.exists()) infoFile.writeText(INFO)
28 | if (!file.exists()) file.createNewFile()
29 | return file
30 | }
31 |
32 | override fun createData(tableName: String, data: ListString): Boolean {
33 | return try {
34 | getFilePath(tableName).writeText(data)
35 | true
36 | } catch (e: Exception) {
37 | e.printStackTrace()
38 | false
39 | }
40 |
41 | }
42 |
43 | override fun deleteData(tableName: String): Boolean {
44 | return try {
45 | getFilePath(tableName).delete()
46 | } catch (e: Exception) {
47 | e.printStackTrace()
48 | false
49 | }
50 | }
51 |
52 | override fun getData(tableName: String): ListString {
53 | return try {
54 | val file = getFilePath(tableName)
55 | file.readText().ifEmpty { "[]" }
56 | } catch (e: Exception) {
57 | println(e.toString())
58 | "[]"
59 | }
60 | }
61 |
62 | override fun getDataFlow(tableName: String): Flow {
63 | return flow {
64 | val json = getData(tableName)
65 | emit(json)
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/DesktopTextToSpeech.kt:
--------------------------------------------------------------------------------
1 | import kotlinx.coroutines.CoroutineScope
2 | import kotlinx.coroutines.Dispatchers
3 | import kotlinx.coroutines.launch
4 | import nl.marc_apps.tts.TextToSpeechFactory
5 | import nl.marc_apps.tts.TextToSpeechInstance
6 |
7 |
8 | class DesktopTextToSpeech : TextToSpeech {
9 | private var textToSpeech: TextToSpeechInstance? = null
10 |
11 | init {
12 | CoroutineScope(Dispatchers.Default).launch {
13 | textToSpeech = TextToSpeechFactory().createOrNull()
14 | }
15 | }
16 |
17 | override suspend fun speak(text: String) {
18 | textToSpeech?.say(text)
19 | }
20 |
21 | override suspend fun stop() {
22 | textToSpeech?.stop()
23 | textToSpeech?.close()
24 | }
25 | }
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/actual.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.graphics.ImageBitmap
2 | import androidx.compose.ui.graphics.toComposeImageBitmap
3 | import com.russhwolf.settings.ObservableSettings
4 | import com.russhwolf.settings.PreferencesSettings
5 | import com.russhwolf.settings.Settings
6 | import org.jetbrains.skia.Image
7 | import java.util.prefs.Preferences
8 |
9 | actual fun getJsonDatabase(): JsonDatabase = DeskTopJsonDB()
10 |
11 | actual fun showAlert(message: String) {
12 | javax.swing.JOptionPane.showMessageDialog(
13 | null,
14 | message,
15 | "Notification",
16 | javax.swing.JOptionPane.INFORMATION_MESSAGE
17 | )
18 | }
19 |
20 | actual fun ByteArray.toComposeImageBitmap(): ImageBitmap =
21 | Image.makeFromEncoded(this).toComposeImageBitmap()
22 |
23 | actual fun getPlatform(): Platform {
24 | return Platform.Desktop(
25 | System.getProperty("os.name") + " " + System.getProperty("os.version")
26 | )
27 | }
28 |
29 | actual fun getDataSettings(): Settings {
30 | return PreferencesSettings(Preferences.userRoot().node("app_preferences"))
31 | }
32 |
33 | actual fun getDataSettingsFlow(): ObservableSettings? {
34 | return PreferencesSettings(Preferences.userRoot().node("app_preferences"))
35 | }
36 |
37 | actual fun getTextToSpeech(): TextToSpeech = DesktopTextToSpeech()
38 |
--------------------------------------------------------------------------------
/composeApp/src/desktopMain/kotlin/main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.desktop.ui.tooling.preview.Preview
2 | import androidx.compose.runtime.Composable
3 | import androidx.compose.ui.window.Window
4 | import androidx.compose.ui.window.application
5 |
6 |
7 | fun main() = application {
8 | Window(onCloseRequest = ::exitApplication, title = "GeminiKMP") {
9 | App()
10 | }
11 | }
12 |
13 | @Preview
14 | @Composable
15 | fun AppDesktopPreview() {
16 | App()
17 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/IosJsonDB.kt:
--------------------------------------------------------------------------------
1 | import kotlinx.cinterop.BetaInteropApi
2 | import kotlinx.cinterop.ExperimentalForeignApi
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.flow
5 | import kotlinx.serialization.json.Json
6 | import platform.Foundation.NSDocumentDirectory
7 | import platform.Foundation.NSFileManager
8 | import platform.Foundation.NSSearchPathForDirectoriesInDomains
9 | import platform.Foundation.NSString
10 | import platform.Foundation.NSUTF8StringEncoding
11 | import platform.Foundation.NSUserDomainMask
12 | import platform.Foundation.create
13 | import platform.Foundation.dataUsingEncoding
14 | import platform.Foundation.stringWithContentsOfFile
15 | import platform.Foundation.writeToFile
16 |
17 | class IosJsonDB : JsonDatabase {
18 |
19 |
20 | private fun getFilePath(tableName: String): String {
21 | val dir = NSSearchPathForDirectoriesInDomains(
22 | directory = NSDocumentDirectory,
23 | domainMask = NSUserDomainMask,
24 | expandTilde = true
25 | ).first() as String
26 | return "$dir/$tableName"
27 | }
28 |
29 | @OptIn(BetaInteropApi::class)
30 | override fun createData(tableName: String, data: ListString): Boolean {
31 | return try {
32 | val json = Json.encodeToString(data)
33 | val path = getFilePath(tableName)
34 | val nsData = NSString.create(string = json).dataUsingEncoding(NSUTF8StringEncoding)
35 | nsData?.writeToFile(path, atomically = true) ?: false
36 | } catch (e: Exception) {
37 | println("createData error: ${e.message}")
38 | false
39 | }
40 | }
41 |
42 | @OptIn(ExperimentalForeignApi::class)
43 | override fun deleteData(tableName: String): Boolean {
44 | return try {
45 | val path = getFilePath(tableName)
46 | val fileManager = NSFileManager.defaultManager
47 | fileManager.removeItemAtPath(path, null)
48 | true
49 | } catch (e: Exception) {
50 | println("deleteData error: ${e.message}")
51 | false
52 | }
53 | }
54 |
55 | @OptIn(ExperimentalForeignApi::class)
56 | override fun getData(tableName: String): ListString {
57 | return try {
58 | val path = getFilePath(tableName)
59 | val nsString = NSString.stringWithContentsOfFile(
60 | path,
61 | encoding = NSUTF8StringEncoding,
62 | error = null
63 | )
64 | if (nsString != null) {
65 | Json.decodeFromString(nsString)
66 | }else "[]"
67 | } catch (e: Exception) {
68 | println("readRawJson error: ${e.message}")
69 | "[]"
70 | }
71 | }
72 |
73 | override fun getDataFlow(tableName: String): Flow {
74 | return flow {
75 | val json = getData(tableName)
76 | emit(json)
77 | }
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/IosTextToSpeech.kt:
--------------------------------------------------------------------------------
1 | import platform.AVFAudio.AVSpeechSynthesisVoice
2 | import platform.AVFAudio.AVSpeechSynthesizer
3 | import platform.AVFAudio.AVSpeechUtterance
4 |
5 |
6 |
7 |
8 | class IosTextToSpeech : TextToSpeech {
9 | private val synthesizer = AVSpeechSynthesizer()
10 |
11 | override suspend fun speak(text: String) {
12 | val utterance = AVSpeechUtterance.speechUtteranceWithString(text)
13 | // utterance.voice = AVSpeechSynthesisVoice().voiceWithLanguage("en-US")
14 | synthesizer.speakUtterance(utterance)
15 | }
16 |
17 | override suspend fun stop() {
18 | synthesizer.stopSpeakingAtBoundary(platform.AVFAudio.AVSpeechBoundary.AVSpeechBoundaryWord)
19 | }
20 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/MainViewController.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.window.ComposeUIViewController
2 |
3 | fun MainViewController() = ComposeUIViewController { App() }
4 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/actual.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.graphics.ImageBitmap
2 | import androidx.compose.ui.graphics.toComposeImageBitmap
3 | import com.russhwolf.settings.NSUserDefaultsSettings
4 | import com.russhwolf.settings.ObservableSettings
5 | import com.russhwolf.settings.Settings
6 | import org.jetbrains.skia.Image
7 | import platform.Foundation.NSUserDefaults
8 | import platform.UIKit.UIAlertController
9 | import platform.UIKit.UIApplication
10 | import platform.UIKit.UIDevice
11 |
12 | actual fun getJsonDatabase(): JsonDatabase = IosJsonDB()
13 |
14 | actual fun showAlert(message: String) {
15 | val alertController = UIAlertController
16 | .alertControllerWithTitle(null, message, 1000L)
17 | val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController
18 | rootViewController?.presentViewController(alertController, true, null)
19 |
20 | }
21 |
22 | actual fun ByteArray.toComposeImageBitmap(): ImageBitmap {
23 | return Image.makeFromEncoded(this).toComposeImageBitmap()
24 | }
25 |
26 | actual fun getPlatform(): Platform {
27 | return Platform.Ios(
28 | UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
29 | )
30 | }
31 |
32 | actual fun getDataSettings(): Settings {
33 | return NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults)
34 | }
35 |
36 | actual fun getDataSettingsFlow(): ObservableSettings? {
37 | return NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults)
38 | }
39 |
40 |
41 | actual fun getTextToSpeech(): TextToSpeech = IosTextToSpeech()
42 |
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/WebJsonDB.kt:
--------------------------------------------------------------------------------
1 |
2 | import kotlinx.browser.localStorage
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.flow
5 | import kotlinx.serialization.json.Json
6 |
7 | class WebJsonDB : JsonDatabase {
8 |
9 | override fun createData(tableName: String, data: ListString): Boolean {
10 | return try {
11 | val json = Json.encodeToString(data)
12 | localStorage.setItem(tableName, json)
13 | true
14 | } catch (e: Exception) {
15 | println("createData error $e")
16 | false
17 | }
18 | }
19 |
20 | override fun deleteData(tableName: String): Boolean {
21 | return try {
22 | localStorage.removeItem(tableName)
23 | true
24 | } catch (e: Exception) {
25 | println("createData error $e")
26 | false
27 | }
28 | }
29 |
30 | override fun getData(tableName: String): ListString {
31 | return try {
32 | val jsonString = localStorage.getItem(tableName) ?: "[]"
33 | Json.decodeFromString(jsonString)
34 | } catch (e: Exception) {
35 | println("createData error $e")
36 | "[]"
37 | }
38 | }
39 |
40 | override fun getDataFlow(tableName: String): Flow {
41 | return flow {
42 | val json = getData(tableName)
43 | emit(json)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/WebTextToSpeech.kt:
--------------------------------------------------------------------------------
1 | import kotlinx.coroutines.CoroutineScope
2 | import kotlinx.coroutines.Dispatchers
3 | import kotlinx.coroutines.launch
4 | import nl.marc_apps.tts.TextToSpeechFactory
5 | import nl.marc_apps.tts.TextToSpeechInstance
6 |
7 |
8 | class WebTextToSpeech : TextToSpeech {
9 | private var textToSpeech: TextToSpeechInstance? = null
10 |
11 | init {
12 | CoroutineScope(Dispatchers.Default).launch {
13 | textToSpeech = TextToSpeechFactory().createOrNull()
14 | }
15 | }
16 |
17 | override suspend fun speak(text: String) {
18 | textToSpeech?.say(text)
19 | }
20 |
21 | override suspend fun stop() {
22 | textToSpeech?.stop()
23 | textToSpeech?.close()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/actual.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.graphics.ImageBitmap
2 | import androidx.compose.ui.graphics.toComposeImageBitmap
3 | import com.russhwolf.settings.ObservableSettings
4 | import com.russhwolf.settings.Settings
5 | import com.russhwolf.settings.StorageSettings
6 | import kotlinx.browser.window
7 | import org.jetbrains.skia.Image
8 |
9 |
10 | //actual suspend fun createDatabaseDriver(): MySqlDriver {
11 | //// return WebWorkerDriver(
12 | //// Worker(
13 | //// js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)""") as String
14 | //// )
15 | //// ).also { ChatDatabase.Schema.awaitCreate(it) }
16 | // throw NotImplementedError("createDatabaseDriver is not implemented on wasm, because sqldriver exist for only js")
17 | //}
18 | //actual suspend fun chatDatabase(): ChatDatabase? = null
19 |
20 | actual fun getJsonDatabase(): JsonDatabase = WebJsonDB()
21 |
22 | actual fun showAlert(message: String) {
23 | window.alert(message)
24 | }
25 |
26 |
27 | actual fun ByteArray.toComposeImageBitmap(): ImageBitmap {
28 | return Image.makeFromEncoded(this).toComposeImageBitmap()
29 | }
30 |
31 | actual fun getPlatform(): Platform {
32 | return Platform.Web("Web wasm")
33 | }
34 |
35 |
36 | actual fun getDataSettings(): Settings {
37 | return StorageSettings()
38 | }
39 |
40 | actual fun getDataSettingsFlow(): ObservableSettings? {
41 | return null
42 | }
43 |
44 | actual fun getTextToSpeech(): TextToSpeech = WebTextToSpeech()
45 |
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/kotlin/main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.ExperimentalComposeUiApi
2 | import androidx.compose.ui.window.CanvasBasedWindow
3 |
4 | @OptIn(ExperimentalComposeUiApi::class)
5 | fun main() {
6 | CanvasBasedWindow(canvasElementId = "ComposeTarget") {
7 | App()
8 | }
9 | }
10 | // For jsMain
11 | //@OptIn(ExperimentalComposeUiApi::class)
12 | //fun main() {
13 | // onWasmReady {
14 | // val body = document.body ?: return@onWasmReady
15 | // CanvasBasedWindow(canvasElementId = "ComposeTarget") {
16 | //// ComposeViewport(body) {
17 | // App()
18 | // }
19 | // }
20 | //}
--------------------------------------------------------------------------------
/composeApp/src/wasmJsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Compose App
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
3 | #Gradle
4 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
5 |
6 |
7 | #Android
8 | android.nonTransitiveRClass=true
9 | android.useAndroidX=true
10 |
11 | #Compose
12 | org.jetbrains.compose.experimental.jscanvas.enabled=true
13 | org.jetbrains.compose.experimental.wasm.enabled=true
14 |
15 | #MPP
16 | kotlin.mpp.androidSourceSetLayoutVersion=2
17 | kotlin.mpp.enableCInteropCommonization=true
18 |
19 | #Development
20 | development=true
21 | kotlin.native.ignoreDisabledTargets=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "2.1.20"
3 | kotlinx-coroutines = "1.10.1"
4 |
5 | agp = "8.9.1"
6 | android-compileSdk = "34"
7 | android-minSdk = "24"
8 | android-targetSdk = "34"
9 | androidx-activityCompose = "1.10.1"
10 | compose = "1.7.8"
11 | compose-plugin = "1.7.1"
12 | composeWindowSize = "0.5.0"
13 | filekit = "0.10.0-beta01"
14 | generativeai = "0.9.0-1.1.0"
15 | horologist = "0.6.20"
16 | junit = "4.13.2"
17 | multiplatformSettings = "1.3.0"
18 | buildkonfig = "0.15.2"
19 | markdownRenderer = "0.27.0"
20 | wearCompose = "1.4.1"
21 | voyagerVersion = "1.1.0-beta03"
22 | kotlinx-serialization = "1.8.1"
23 |
24 |
25 | [libraries]
26 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
27 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
28 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
29 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
30 |
31 | generativeai = { module = "dev.shreyaspatil.generativeai:generativeai-google", version.ref = "generativeai" }
32 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
33 |
34 | androidx-wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "wearCompose" }
35 |
36 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
37 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
38 | compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
39 | compose-window-size = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "composeWindowSize" }
40 |
41 | filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekit" }
42 | filekit-coil = { module = "io.github.vinceglb:filekit-coil", version.ref = "filekit" }
43 |
44 | markdown-renderer = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" }
45 | markdown-renderer-core = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "markdownRenderer" }
46 |
47 | horologist-composables = { module = "com.google.android.horologist:horologist-composables", version.ref = "horologist" }
48 | horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" }
49 | horologist-ai-ui = { module = "com.google.android.horologist:horologist-ai-ui", version.ref = "horologist" }
50 | compose-material-iconsext = "androidx.compose.material:material-icons-extended:1.7.8"
51 |
52 | desugar = "com.android.tools:desugar_jdk_libs:2.1.2"
53 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
54 | multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" }
55 | multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatformSettings" }
56 | voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyagerVersion" }
57 | voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyagerVersion" }
58 |
59 | [plugins]
60 | androidApplication = { id = "com.android.application", version.ref = "agp" }
61 | androidLibrary = { id = "com.android.library", version.ref = "agp" }
62 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
63 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
64 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
65 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
66 | buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig" }
67 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Apr 04 12:55:16 WAT 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Use the maximum available, or set MAX_FD != -1 to use that value.
89 | MAX_FD=maximum
90 |
91 | warn () {
92 | echo "$*"
93 | } >&2
94 |
95 | die () {
96 | echo
97 | echo "$*"
98 | echo
99 | exit 1
100 | } >&2
101 |
102 | # OS specific support (must be 'true' or 'false').
103 | cygwin=false
104 | msys=false
105 | darwin=false
106 | nonstop=false
107 | case "$( uname )" in #(
108 | CYGWIN* ) cygwin=true ;; #(
109 | Darwin* ) darwin=true ;; #(
110 | MSYS* | MINGW* ) msys=true ;; #(
111 | NONSTOP* ) nonstop=true ;;
112 | esac
113 |
114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
115 |
116 |
117 | # Determine the Java command to use to start the JVM.
118 | if [ -n "$JAVA_HOME" ] ; then
119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
120 | # IBM's JDK on AIX uses strange locations for the executables
121 | JAVACMD=$JAVA_HOME/jre/sh/java
122 | else
123 | JAVACMD=$JAVA_HOME/bin/java
124 | fi
125 | if [ ! -x "$JAVACMD" ] ; then
126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
127 |
128 | Please set the JAVA_HOME variable in your environment to match the
129 | location of your Java installation."
130 | fi
131 | else
132 | JAVACMD=java
133 | if ! command -v java >/dev/null 2>&1
134 | then
135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
136 |
137 | Please set the JAVA_HOME variable in your environment to match the
138 | location of your Java installation."
139 | fi
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
147 | # shellcheck disable=SC3045
148 | MAX_FD=$( ulimit -H -n ) ||
149 | warn "Could not query maximum file descriptor limit"
150 | esac
151 | case $MAX_FD in #(
152 | '' | soft) :;; #(
153 | *)
154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
155 | # shellcheck disable=SC3045
156 | ulimit -n "$MAX_FD" ||
157 | warn "Could not set maximum file descriptor limit to $MAX_FD"
158 | esac
159 | fi
160 |
161 | # Collect all arguments for the java command, stacking in reverse order:
162 | # * args from the command line
163 | # * the main class name
164 | # * -classpath
165 | # * -D...appname settings
166 | # * --module-path (only if needed)
167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
168 |
169 | # For Cygwin or MSYS, switch paths to Windows format before running java
170 | if "$cygwin" || "$msys" ; then
171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
173 |
174 | JAVACMD=$( cygpath --unix "$JAVACMD" )
175 |
176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
177 | for arg do
178 | if
179 | case $arg in #(
180 | -*) false ;; # don't mess with options #(
181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
182 | [ -e "$t" ] ;; #(
183 | *) false ;;
184 | esac
185 | then
186 | arg=$( cygpath --path --ignore --mixed "$arg" )
187 | fi
188 | # Roll the args list around exactly as many times as the number of
189 | # args, so each arg winds up back in the position where it started, but
190 | # possibly modified.
191 | #
192 | # NB: a `for` loop captures its iteration list before it begins, so
193 | # changing the positional parameters here affects neither the number of
194 | # iterations, nor the values presented in `arg`.
195 | shift # remove old arg
196 | set -- "$@" "$arg" # push replacement arg
197 | done
198 | fi
199 |
200 |
201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
203 |
204 | # Collect all arguments for the java command;
205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
206 | # shell script including quotes and variable substitutions, so put them in
207 | # double quotes to make sure that they get re-expanded; and
208 | # * put everything else in single quotes, so that it's not re-expanded.
209 |
210 | set -- \
211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
212 | -classpath "$CLASSPATH" \
213 | org.gradle.wrapper.GradleWrapperMain \
214 | "$@"
215 |
216 | # Stop when "xargs" is not available.
217 | if ! command -v xargs >/dev/null 2>&1
218 | then
219 | die "xargs is not available"
220 | fi
221 |
222 | # Use "xargs" to parse quoted args.
223 | #
224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
225 | #
226 | # In Bash we could simply go:
227 | #
228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
229 | # set -- "${ARGS[@]}" "$@"
230 | #
231 | # but POSIX shell has neither arrays nor command substitution, so instead we
232 | # post-process each arg (as a line of input to sed) to backslash-escape any
233 | # character that might be a shell metacharacter, then use eval to reverse
234 | # that process (while maintaining the separation between arguments), and wrap
235 | # the whole thing up as a single "set" statement.
236 | #
237 | # This will of course break if any of these variables contains a newline or
238 | # an unmatched quote.
239 | #
240 |
241 | eval "set -- $(
242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
243 | xargs -n1 |
244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
245 | tr '\n' ' '
246 | )" '"$@"'
247 |
248 | exec "$JAVACMD" "$@"
249 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=dev.johnoreilly.gemini.GeminiKMP
3 | APP_NAME=GeminiKMP
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
11 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
12 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
13 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
14 | /* End PBXBuildFile section */
15 |
16 | /* Begin PBXFileReference section */
17 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
18 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
19 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
20 | 7555FF7B242A565900829871 /* GeminiKMP.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GeminiKMP.app; sourceTree = BUILT_PRODUCTS_DIR; };
21 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
22 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
23 | AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
24 | /* End PBXFileReference section */
25 |
26 | /* Begin PBXGroup section */
27 | 058557D7273AAEEB004C7B11 /* Preview Content */ = {
28 | isa = PBXGroup;
29 | children = (
30 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
31 | );
32 | path = "Preview Content";
33 | sourceTree = "";
34 | };
35 | 42799AB246E5F90AF97AA0EF /* Frameworks */ = {
36 | isa = PBXGroup;
37 | children = (
38 | );
39 | name = Frameworks;
40 | sourceTree = "";
41 | };
42 | 7555FF72242A565900829871 = {
43 | isa = PBXGroup;
44 | children = (
45 | AB1DB47929225F7C00F7AF9C /* Configuration */,
46 | 7555FF7D242A565900829871 /* iosApp */,
47 | 7555FF7C242A565900829871 /* Products */,
48 | 42799AB246E5F90AF97AA0EF /* Frameworks */,
49 | );
50 | sourceTree = "";
51 | };
52 | 7555FF7C242A565900829871 /* Products */ = {
53 | isa = PBXGroup;
54 | children = (
55 | 7555FF7B242A565900829871 /* GeminiKMP.app */,
56 | );
57 | name = Products;
58 | sourceTree = "";
59 | };
60 | 7555FF7D242A565900829871 /* iosApp */ = {
61 | isa = PBXGroup;
62 | children = (
63 | 058557BA273AAA24004C7B11 /* Assets.xcassets */,
64 | 7555FF82242A565900829871 /* ContentView.swift */,
65 | 7555FF8C242A565B00829871 /* Info.plist */,
66 | 2152FB032600AC8F00CF470E /* iOSApp.swift */,
67 | 058557D7273AAEEB004C7B11 /* Preview Content */,
68 | );
69 | path = iosApp;
70 | sourceTree = "";
71 | };
72 | AB1DB47929225F7C00F7AF9C /* Configuration */ = {
73 | isa = PBXGroup;
74 | children = (
75 | AB3632DC29227652001CCB65 /* Config.xcconfig */,
76 | );
77 | path = Configuration;
78 | sourceTree = "";
79 | };
80 | /* End PBXGroup section */
81 |
82 | /* Begin PBXNativeTarget section */
83 | 7555FF7A242A565900829871 /* iosApp */ = {
84 | isa = PBXNativeTarget;
85 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
86 | buildPhases = (
87 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */,
88 | 7555FF77242A565900829871 /* Sources */,
89 | 7555FF79242A565900829871 /* Resources */,
90 | );
91 | buildRules = (
92 | );
93 | dependencies = (
94 | );
95 | name = iosApp;
96 | productName = iosApp;
97 | productReference = 7555FF7B242A565900829871 /* GeminiKMP.app */;
98 | productType = "com.apple.product-type.application";
99 | };
100 | /* End PBXNativeTarget section */
101 |
102 | /* Begin PBXProject section */
103 | 7555FF73242A565900829871 /* Project object */ = {
104 | isa = PBXProject;
105 | attributes = {
106 | LastSwiftUpdateCheck = 1130;
107 | LastUpgradeCheck = 1130;
108 | ORGANIZATIONNAME = orgName;
109 | TargetAttributes = {
110 | 7555FF7A242A565900829871 = {
111 | CreatedOnToolsVersion = 11.3.1;
112 | };
113 | };
114 | };
115 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
116 | compatibilityVersion = "Xcode 9.3";
117 | developmentRegion = en;
118 | hasScannedForEncodings = 0;
119 | knownRegions = (
120 | en,
121 | Base,
122 | );
123 | mainGroup = 7555FF72242A565900829871;
124 | productRefGroup = 7555FF7C242A565900829871 /* Products */;
125 | projectDirPath = "";
126 | projectRoot = "";
127 | targets = (
128 | 7555FF7A242A565900829871 /* iosApp */,
129 | );
130 | };
131 | /* End PBXProject section */
132 |
133 | /* Begin PBXResourcesBuildPhase section */
134 | 7555FF79242A565900829871 /* Resources */ = {
135 | isa = PBXResourcesBuildPhase;
136 | buildActionMask = 2147483647;
137 | files = (
138 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
139 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
140 | );
141 | runOnlyForDeploymentPostprocessing = 0;
142 | };
143 | /* End PBXResourcesBuildPhase section */
144 |
145 | /* Begin PBXShellScriptBuildPhase section */
146 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = {
147 | isa = PBXShellScriptBuildPhase;
148 | buildActionMask = 2147483647;
149 | files = (
150 | );
151 | inputFileListPaths = (
152 | );
153 | inputPaths = (
154 | );
155 | name = "Compile Kotlin Framework";
156 | outputFileListPaths = (
157 | );
158 | outputPaths = (
159 | );
160 | runOnlyForDeploymentPostprocessing = 0;
161 | shellPath = /bin/sh;
162 | shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
163 | };
164 | /* End PBXShellScriptBuildPhase section */
165 |
166 | /* Begin PBXSourcesBuildPhase section */
167 | 7555FF77242A565900829871 /* Sources */ = {
168 | isa = PBXSourcesBuildPhase;
169 | buildActionMask = 2147483647;
170 | files = (
171 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
172 | 7555FF83242A565900829871 /* ContentView.swift in Sources */,
173 | );
174 | runOnlyForDeploymentPostprocessing = 0;
175 | };
176 | /* End PBXSourcesBuildPhase section */
177 |
178 | /* Begin XCBuildConfiguration section */
179 | 7555FFA3242A565B00829871 /* Debug */ = {
180 | isa = XCBuildConfiguration;
181 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
182 | buildSettings = {
183 | ALWAYS_SEARCH_USER_PATHS = NO;
184 | CLANG_ANALYZER_NONNULL = YES;
185 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
186 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
187 | CLANG_CXX_LIBRARY = "libc++";
188 | CLANG_ENABLE_MODULES = YES;
189 | CLANG_ENABLE_OBJC_ARC = YES;
190 | CLANG_ENABLE_OBJC_WEAK = YES;
191 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
192 | CLANG_WARN_BOOL_CONVERSION = YES;
193 | CLANG_WARN_COMMA = YES;
194 | CLANG_WARN_CONSTANT_CONVERSION = YES;
195 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
196 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
197 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
198 | CLANG_WARN_EMPTY_BODY = YES;
199 | CLANG_WARN_ENUM_CONVERSION = YES;
200 | CLANG_WARN_INFINITE_RECURSION = YES;
201 | CLANG_WARN_INT_CONVERSION = YES;
202 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
203 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
204 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
205 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
206 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
207 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
208 | CLANG_WARN_STRICT_PROTOTYPES = YES;
209 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
210 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
211 | CLANG_WARN_UNREACHABLE_CODE = YES;
212 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
213 | COPY_PHASE_STRIP = NO;
214 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
215 | ENABLE_STRICT_OBJC_MSGSEND = YES;
216 | ENABLE_TESTABILITY = YES;
217 | GCC_C_LANGUAGE_STANDARD = gnu11;
218 | GCC_DYNAMIC_NO_PIC = NO;
219 | GCC_NO_COMMON_BLOCKS = YES;
220 | GCC_OPTIMIZATION_LEVEL = 0;
221 | GCC_PREPROCESSOR_DEFINITIONS = (
222 | "DEBUG=1",
223 | "$(inherited)",
224 | );
225 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
226 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
227 | GCC_WARN_UNDECLARED_SELECTOR = YES;
228 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
229 | GCC_WARN_UNUSED_FUNCTION = YES;
230 | GCC_WARN_UNUSED_VARIABLE = YES;
231 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
232 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
233 | MTL_FAST_MATH = YES;
234 | ONLY_ACTIVE_ARCH = YES;
235 | SDKROOT = iphoneos;
236 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
237 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
238 | };
239 | name = Debug;
240 | };
241 | 7555FFA4242A565B00829871 /* Release */ = {
242 | isa = XCBuildConfiguration;
243 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
244 | buildSettings = {
245 | ALWAYS_SEARCH_USER_PATHS = NO;
246 | CLANG_ANALYZER_NONNULL = YES;
247 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
249 | CLANG_CXX_LIBRARY = "libc++";
250 | CLANG_ENABLE_MODULES = YES;
251 | CLANG_ENABLE_OBJC_ARC = YES;
252 | CLANG_ENABLE_OBJC_WEAK = YES;
253 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
254 | CLANG_WARN_BOOL_CONVERSION = YES;
255 | CLANG_WARN_COMMA = YES;
256 | CLANG_WARN_CONSTANT_CONVERSION = YES;
257 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
258 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
259 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
260 | CLANG_WARN_EMPTY_BODY = YES;
261 | CLANG_WARN_ENUM_CONVERSION = YES;
262 | CLANG_WARN_INFINITE_RECURSION = YES;
263 | CLANG_WARN_INT_CONVERSION = YES;
264 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
265 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
266 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
267 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
268 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
269 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
270 | CLANG_WARN_STRICT_PROTOTYPES = YES;
271 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
272 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
273 | CLANG_WARN_UNREACHABLE_CODE = YES;
274 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
275 | COPY_PHASE_STRIP = NO;
276 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
277 | ENABLE_NS_ASSERTIONS = NO;
278 | ENABLE_STRICT_OBJC_MSGSEND = YES;
279 | GCC_C_LANGUAGE_STANDARD = gnu11;
280 | GCC_NO_COMMON_BLOCKS = YES;
281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
283 | GCC_WARN_UNDECLARED_SELECTOR = YES;
284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
285 | GCC_WARN_UNUSED_FUNCTION = YES;
286 | GCC_WARN_UNUSED_VARIABLE = YES;
287 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
288 | MTL_ENABLE_DEBUG_INFO = NO;
289 | MTL_FAST_MATH = YES;
290 | SDKROOT = iphoneos;
291 | SWIFT_COMPILATION_MODE = wholemodule;
292 | SWIFT_OPTIMIZATION_LEVEL = "-O";
293 | VALIDATE_PRODUCT = YES;
294 | };
295 | name = Release;
296 | };
297 | 7555FFA6242A565B00829871 /* Debug */ = {
298 | isa = XCBuildConfiguration;
299 | buildSettings = {
300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
301 | CODE_SIGN_IDENTITY = "Apple Development";
302 | CODE_SIGN_STYLE = Manual;
303 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
304 | DEVELOPMENT_TEAM = "";
305 | ENABLE_PREVIEWS = YES;
306 | FRAMEWORK_SEARCH_PATHS = (
307 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
308 | );
309 | INFOPLIST_FILE = iosApp/Info.plist;
310 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
311 | LD_RUNPATH_SEARCH_PATHS = (
312 | "$(inherited)",
313 | "@executable_path/Frameworks",
314 | );
315 | OTHER_LDFLAGS = (
316 | "$(inherited)",
317 | "-framework",
318 | composeApp,
319 | );
320 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
321 | PRODUCT_NAME = "${APP_NAME}";
322 | PROVISIONING_PROFILE_SPECIFIER = "";
323 | SWIFT_VERSION = 5.0;
324 | TARGETED_DEVICE_FAMILY = "1,2";
325 | };
326 | name = Debug;
327 | };
328 | 7555FFA7242A565B00829871 /* Release */ = {
329 | isa = XCBuildConfiguration;
330 | buildSettings = {
331 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
332 | CODE_SIGN_IDENTITY = "Apple Development";
333 | CODE_SIGN_STYLE = Automatic;
334 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
335 | DEVELOPMENT_TEAM = NT77748GS8;
336 | ENABLE_PREVIEWS = YES;
337 | FRAMEWORK_SEARCH_PATHS = (
338 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
339 | );
340 | INFOPLIST_FILE = iosApp/Info.plist;
341 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
342 | LD_RUNPATH_SEARCH_PATHS = (
343 | "$(inherited)",
344 | "@executable_path/Frameworks",
345 | );
346 | OTHER_LDFLAGS = (
347 | "$(inherited)",
348 | "-framework",
349 | composeApp,
350 | );
351 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
352 | PRODUCT_NAME = "${APP_NAME}";
353 | PROVISIONING_PROFILE_SPECIFIER = "";
354 | SWIFT_VERSION = 5.0;
355 | TARGETED_DEVICE_FAMILY = "1,2";
356 | };
357 | name = Release;
358 | };
359 | /* End XCBuildConfiguration section */
360 |
361 | /* Begin XCConfigurationList section */
362 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
363 | isa = XCConfigurationList;
364 | buildConfigurations = (
365 | 7555FFA3242A565B00829871 /* Debug */,
366 | 7555FFA4242A565B00829871 /* Release */,
367 | );
368 | defaultConfigurationIsVisible = 0;
369 | defaultConfigurationName = Release;
370 | };
371 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
372 | isa = XCConfigurationList;
373 | buildConfigurations = (
374 | 7555FFA6242A565B00829871 /* Debug */,
375 | 7555FFA7242A565B00829871 /* Release */,
376 | );
377 | defaultConfigurationIsVisible = 0;
378 | defaultConfigurationName = Release;
379 | };
380 | /* End XCConfigurationList section */
381 | };
382 | rootObject = 7555FF73242A565900829871 /* Project object */;
383 | }
384 |
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/joreilly.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/joreilly.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/xcuserdata/joreilly.xcuserdatad/xcschemes/iosApp.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
14 |
20 |
21 |
22 |
23 |
24 |
30 |
31 |
41 |
43 |
49 |
50 |
51 |
52 |
58 |
59 |
61 |
62 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/xcuserdata/joreilly.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | iosApp.xcscheme
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import ComposeApp
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | MainViewControllerKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
17 | }
18 | }
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | CADisableMinimumFrameDurationOnPhone
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct iOSApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "GeminiKMP"
2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3 |
4 | pluginManagement {
5 | repositories {
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | google()
8 | gradlePluginPortal()
9 | mavenCentral()
10 | }
11 | }
12 |
13 | dependencyResolutionManagement {
14 | repositories {
15 | google()
16 | mavenCentral()
17 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
18 | maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental")
19 | }
20 | }
21 |
22 | include(":composeApp")
23 | include(":wearApp")
--------------------------------------------------------------------------------
/wearApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | import java.util.Properties
4 |
5 | plugins {
6 | alias(libs.plugins.androidApplication)
7 | kotlin("android")
8 | alias(libs.plugins.kotlinx.serialization)
9 | alias(libs.plugins.compose.compiler)
10 | }
11 |
12 | android {
13 | compileSdk = 35
14 | defaultConfig {
15 | applicationId = "dev.johnoreilly.gemini"
16 | minSdk = 30
17 | targetSdk = 33
18 | versionCode = 1
19 | versionName = "1.0"
20 |
21 | val localPropsFile = rootProject.file("local.properties")
22 | val localProperties = Properties()
23 | if (localPropsFile.exists()) {
24 | runCatching {
25 | localProperties.load(localPropsFile.inputStream())
26 | }.getOrElse {
27 | it.printStackTrace()
28 | }
29 | }
30 |
31 | buildConfigField(
32 | "String",
33 | "GEMINI_API_KEY",
34 | "\"" + localProperties["GEMINI_API_KEY"] + "\"",
35 | )
36 | }
37 |
38 | compileOptions {
39 | sourceCompatibility = JavaVersion.VERSION_1_8
40 | targetCompatibility = JavaVersion.VERSION_1_8
41 | isCoreLibraryDesugaringEnabled = true
42 | }
43 |
44 | buildFeatures {
45 | compose = true
46 | buildConfig = true
47 | }
48 |
49 | testOptions {
50 | unitTests {
51 | isIncludeAndroidResources = true
52 | }
53 | }
54 |
55 | kotlinOptions {
56 | this.jvmTarget = "1.8"
57 | freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
58 | freeCompilerArgs += "-opt-in=com.google.android.horologist.annotations.ExperimentalHorologistApi"
59 | }
60 |
61 | packaging {
62 | resources {
63 | excludes += listOf(
64 | "/META-INF/AL2.0",
65 | "/META-INF/LGPL2.1",
66 | "/META-INF/versions/**"
67 | )
68 | }
69 | }
70 |
71 | namespace = "dev.johnoreilly.gemini"
72 | }
73 |
74 | kotlin {
75 | sourceSets.all {
76 | languageSettings {
77 | optIn("kotlin.RequiresOptIn")
78 | }
79 | }
80 | }
81 |
82 | dependencies {
83 | implementation(libs.androidx.activity.compose)
84 | implementation(libs.androidx.wear.compose.navigation)
85 |
86 | implementation(libs.compose.foundation)
87 | implementation(libs.horologist.composables)
88 | implementation(libs.horologist.compose.layout)
89 | implementation(libs.horologist.ai.ui)
90 | implementation(libs.markdown.renderer.core)
91 | implementation(libs.compose.material.iconsext)
92 | implementation(libs.kotlinx.coroutines.core)
93 | implementation(libs.generativeai)
94 | coreLibraryDesugaring(libs.desugar)
95 | }
96 |
--------------------------------------------------------------------------------
/wearApp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/wearApp/src/main/kotlin/dev/johnoreilly/gemini/common/GeminiApi.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini.common
2 |
3 | import dev.johnoreilly.gemini.BuildConfig
4 | import dev.shreyaspatil.ai.client.generativeai.GenerativeModel
5 | import dev.shreyaspatil.ai.client.generativeai.type.GenerateContentResponse
6 |
7 | class GeminiApi {
8 | val generativeModel = GenerativeModel(
9 | modelName = "gemini-pro",
10 | apiKey = BuildConfig.GEMINI_API_KEY
11 | )
12 |
13 | suspend fun generateContent(prompt: String): GenerateContentResponse {
14 | return generativeModel.generateContent(prompt)
15 | }
16 | }
--------------------------------------------------------------------------------
/wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini.wear
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 |
7 | class MainActivity : ComponentActivity() {
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 |
11 | setContent {
12 | WearApp()
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/Screen.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini.wear
2 |
3 | sealed class Screen(
4 | val route: String,
5 | ) {
6 | object PromptScreen : Screen("prompt")
7 | }
--------------------------------------------------------------------------------
/wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/WearApp.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini.wear
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.navigation.NavHostController
6 | import androidx.wear.compose.navigation.SwipeDismissableNavHost
7 | import androidx.wear.compose.navigation.composable
8 | import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
9 | import com.google.android.horologist.compose.layout.AppScaffold
10 | import dev.johnoreilly.gemini.wear.prompt.GeminiPromptScreen
11 |
12 |
13 | @Composable
14 | fun WearApp(
15 | modifier: Modifier = Modifier,
16 | navController: NavHostController = rememberSwipeDismissableNavController(),
17 | ) {
18 | AppScaffold(modifier = modifier) {
19 | SwipeDismissableNavHost(
20 | startDestination = Screen.PromptScreen.route,
21 | navController = navController,
22 | ) {
23 | composable(
24 | route = Screen.PromptScreen.route,
25 | ) {
26 | GeminiPromptScreen()
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/markdown/WearMaterialTypography.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini.wear.markdown
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.text.SpanStyle
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.font.FontStyle
8 | import androidx.wear.compose.material.LocalContentColor
9 | import androidx.wear.compose.material.MaterialTheme
10 | import com.mikepenz.markdown.model.DefaultMarkdownColors
11 | import com.mikepenz.markdown.model.DefaultMarkdownTypography
12 |
13 | @Composable
14 | fun wearMaterialTypography() = DefaultMarkdownTypography(
15 | h1 = MaterialTheme.typography.title1,
16 | h2 = MaterialTheme.typography.title2,
17 | h3 = MaterialTheme.typography.title3,
18 | h4 = MaterialTheme.typography.caption1,
19 | h5 = MaterialTheme.typography.caption2,
20 | h6 = MaterialTheme.typography.caption3,
21 | text = MaterialTheme.typography.body1,
22 | code = MaterialTheme.typography.body2.copy(fontFamily = FontFamily.Monospace),
23 | quote = MaterialTheme.typography.body2.plus(SpanStyle(fontStyle = FontStyle.Italic)),
24 | paragraph = MaterialTheme.typography.body1,
25 | ordered = MaterialTheme.typography.body1,
26 | bullet = MaterialTheme.typography.body1,
27 | list = MaterialTheme.typography.body1,
28 | inlineCode = MaterialTheme.typography.body1,
29 | link = MaterialTheme.typography.body1
30 | )
31 |
32 | @Composable
33 | fun wearMaterialColors() = DefaultMarkdownColors(
34 | text = Color.White,
35 | codeText = LocalContentColor.current,
36 | linkText = Color.Blue,
37 | codeBackground = MaterialTheme.colors.background,
38 | inlineCodeBackground = MaterialTheme.colors.background,
39 | dividerColor = MaterialTheme.colors.secondaryVariant,
40 | inlineCodeText = MaterialTheme.colors.primary
41 | )
--------------------------------------------------------------------------------
/wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/prompt/GeminiPromptScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini.wear.prompt
2 |
3 | import android.content.Intent
4 | import android.speech.RecognizerIntent
5 | import androidx.activity.compose.rememberLauncherForActivityResult
6 | import androidx.activity.result.contract.ActivityResultContracts
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Mic
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
17 | import androidx.lifecycle.viewmodel.compose.viewModel
18 | import androidx.wear.compose.material.Card
19 | import androidx.wear.compose.material.CardDefaults
20 | import androidx.wear.compose.material.MaterialTheme
21 | import com.google.android.horologist.ai.ui.components.PromptOrResponseDisplay
22 | import com.google.android.horologist.ai.ui.model.PromptOrResponseUiModel
23 | import com.google.android.horologist.ai.ui.model.TextResponseUiModel
24 | import com.google.android.horologist.ai.ui.screens.PromptScreen
25 | import com.google.android.horologist.ai.ui.screens.PromptUiState
26 | import com.google.android.horologist.compose.layout.ScalingLazyColumnState
27 | import com.google.android.horologist.compose.layout.ScreenScaffold
28 | import com.google.android.horologist.compose.layout.rememberColumnState
29 | import com.google.android.horologist.compose.material.Button
30 | import com.mikepenz.markdown.compose.Markdown
31 | import dev.johnoreilly.gemini.R
32 | import dev.johnoreilly.gemini.wear.markdown.wearMaterialColors
33 | import dev.johnoreilly.gemini.wear.markdown.wearMaterialTypography
34 |
35 | @Composable
36 | fun GeminiPromptScreen(
37 | modifier: Modifier = Modifier,
38 | viewModel: GeminiPromptViewModel = viewModel(),
39 | columnState: ScalingLazyColumnState = rememberColumnState()
40 | ) {
41 | val uiState by viewModel.uiState.collectAsStateWithLifecycle()
42 |
43 | val voiceLauncher =
44 | rememberLauncherForActivityResult(
45 | ActivityResultContracts.StartActivityForResult(),
46 | ) {
47 | it.data?.let { data ->
48 | val results = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
49 | val enteredPrompt = results?.get(0)
50 | if (!enteredPrompt.isNullOrBlank()) {
51 | viewModel.askQuestion(enteredPrompt)
52 | }
53 | }
54 | }
55 |
56 | val voiceIntent: Intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
57 | putExtra(
58 | RecognizerIntent.EXTRA_LANGUAGE_MODEL,
59 | RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
60 | )
61 |
62 | putExtra(
63 | RecognizerIntent.EXTRA_PROMPT,
64 | stringResource(R.string.prompt_input),
65 | )
66 | }
67 |
68 | GeminiPromptScreen(
69 | uiState = uiState,
70 | modifier = modifier,
71 | columnState = columnState,
72 | ) {
73 | Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
74 | Button(
75 | Icons.Default.Mic,
76 | contentDescription = stringResource(R.string.prompt_input),
77 | onClick = {
78 | voiceLauncher.launch(voiceIntent)
79 | },
80 | )
81 | }
82 | }
83 | }
84 |
85 | @Composable
86 | private fun GeminiPromptScreen(
87 | uiState: PromptUiState,
88 | modifier: Modifier = Modifier,
89 | columnState: ScalingLazyColumnState = rememberColumnState(),
90 | promptEntry: @Composable () -> Unit,
91 | ) {
92 | ScreenScaffold(scrollState = columnState) {
93 | PromptScreen(
94 | uiState = uiState,
95 | modifier = modifier,
96 | promptEntry = promptEntry,
97 | promptDisplay = { GeminiPromptDisplay(it) }
98 | )
99 | }
100 | }
101 |
102 | @Composable
103 | private fun GeminiPromptDisplay(it: PromptOrResponseUiModel) {
104 | if (it is TextResponseUiModel) {
105 | GeminiTextResponseCard(it)
106 | } else {
107 | PromptOrResponseDisplay(
108 | promptResponse = it,
109 | onClick = {},
110 | )
111 | }
112 | }
113 |
114 | @Composable
115 | fun GeminiTextResponseCard(
116 | textResponseUiModel: TextResponseUiModel,
117 | modifier: Modifier = Modifier,
118 | onClick: () -> Unit = {},
119 | ) {
120 | Card(
121 | modifier = modifier.fillMaxWidth(),
122 | onClick = onClick,
123 | backgroundPainter = CardDefaults.cardBackgroundPainter(
124 | MaterialTheme.colors.surface,
125 | MaterialTheme.colors.surface,
126 | ),
127 | ) {
128 | Markdown(
129 | textResponseUiModel.text,
130 | colors = wearMaterialColors(),
131 | typography = wearMaterialTypography(),
132 | )
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/prompt/GeminiPromptViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.johnoreilly.gemini.wear.prompt
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.google.android.horologist.ai.ui.model.FailedResponseUiModel
6 | import com.google.android.horologist.ai.ui.model.ModelInstanceUiModel
7 | import com.google.android.horologist.ai.ui.model.PromptOrResponseUiModel
8 | import com.google.android.horologist.ai.ui.model.TextPromptUiModel
9 | import com.google.android.horologist.ai.ui.model.TextResponseUiModel
10 | import com.google.android.horologist.ai.ui.screens.PromptUiState
11 | import dev.johnoreilly.gemini.common.GeminiApi
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.SharingStarted
14 | import kotlinx.coroutines.flow.StateFlow
15 | import kotlinx.coroutines.flow.combine
16 | import kotlinx.coroutines.flow.stateIn
17 | import kotlinx.coroutines.flow.update
18 | import kotlinx.coroutines.launch
19 |
20 | class GeminiPromptViewModel : ViewModel() {
21 | val api = GeminiApi()
22 |
23 | val previousQuestions: MutableStateFlow> =
24 | MutableStateFlow(listOf())
25 | val pendingQuestion: MutableStateFlow =
26 | MutableStateFlow(null)
27 |
28 | fun askQuestion(enteredPrompt: String) {
29 | val textPromptUiModel = TextPromptUiModel(enteredPrompt)
30 | pendingQuestion.value = textPromptUiModel
31 |
32 | viewModelScope.launch {
33 | val responseUi = queryForPrompt(enteredPrompt)
34 |
35 | previousQuestions.update {
36 | it + listOf(textPromptUiModel, responseUi)
37 | }
38 |
39 | pendingQuestion.value = null
40 | }
41 | }
42 |
43 | private suspend fun queryForPrompt(
44 | enteredPrompt: String,
45 | ): PromptOrResponseUiModel {
46 | return try {
47 | val text = api.generateContent(enteredPrompt).text ?: error("No results")
48 | TextResponseUiModel(text)
49 | } catch (e: Exception) {
50 | FailedResponseUiModel(e.toString())
51 | }
52 | }
53 |
54 | val uiState: StateFlow =
55 | combine(
56 | previousQuestions,
57 | pendingQuestion,
58 | ) { prev, curr ->
59 | val modelInfo = ModelInstanceUiModel("gemini", "Gemini")
60 | PromptUiState(modelInfo, prev, curr)
61 | }.stateIn(
62 | viewModelScope,
63 | started = SharingStarted.WhileSubscribed(5000),
64 | initialValue = PromptUiState(messages = previousQuestions.value),
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/wearApp/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/wearApp/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/wearApp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | GeminiKMP
3 | Voice Prompt
4 |
--------------------------------------------------------------------------------