diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index a05364a..f582554 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -4,12 +4,15 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
+import com.codingfeline.buildkonfig.compiler.FieldSpec
+import java.util.Properties
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.buildkonfig)
}
kotlin {
@@ -29,11 +32,12 @@ kotlin {
}
binaries.executable()
}
-
+
androidTarget {
- @OptIn(ExperimentalKotlinGradlePluginApi::class)
- compilerOptions {
- jvmTarget.set(JvmTarget.JVM_17)
+ compilations.all {
+ kotlinOptions {
+ jvmTarget = "17"
+ }
}
}
@@ -67,7 +71,12 @@ kotlin {
implementation(libs.coil.network.ktor)
implementation(libs.voyager.navigator)
implementation(libs.voyager.tab.navigator)
+ implementation(libs.voyager.screenmodel)
implementation(libs.lifecycle.viewmodel.compose)
+ implementation(libs.markdown.renderer)
+ api(libs.compose.window.size)
+ api(libs.generativeai)
+ implementation(libs.filekit.compose)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
@@ -147,3 +156,25 @@ compose.desktop {
}
}
}
+
+buildkonfig {
+ packageName = "com.travel.buddy"
+
+ val localPropsFile = rootProject.file("local.properties")
+ val localProperties = Properties()
+ if (localPropsFile.exists()) {
+ runCatching {
+ localProperties.load(localPropsFile.inputStream())
+ }.getOrElse {
+ it.printStackTrace()
+ }
+ }
+ defaultConfigs {
+ buildConfigField(
+ FieldSpec.Type.STRING,
+ "GEMINI_API_KEY",
+ localProperties["gemini_api_key"]?.toString() ?: ""
+ )
+ }
+
+}
diff --git a/composeApp/src/androidMain/kotlin/App.android.kt b/composeApp/src/androidMain/kotlin/App.android.kt
index bac53e4..6925f44 100644
--- a/composeApp/src/androidMain/kotlin/App.android.kt
+++ b/composeApp/src/androidMain/kotlin/App.android.kt
@@ -1,5 +1,8 @@
import android.content.Intent
+import android.graphics.BitmapFactory
import android.net.Uri
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
import com.travel.buddy.MuseumApp
internal actual fun openUrl(url: String?) {
@@ -10,4 +13,8 @@ internal actual fun openUrl(url: String?) {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
MuseumApp.INSTANCE.startActivity(intent)
+}
+
+actual fun ByteArray.toComposeImageBitmap(): ImageBitmap {
+ return BitmapFactory.decodeByteArray(this, 0, size).asImageBitmap()
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/chat_bot.png b/composeApp/src/commonMain/composeResources/drawable/chat_bot.png
new file mode 100644
index 0000000..e26a2ea
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/chat_bot.png differ
diff --git a/composeApp/src/commonMain/composeResources/values/string.xml b/composeApp/src/commonMain/composeResources/values/string.xml
index b094b75..613b3c5 100644
--- a/composeApp/src/commonMain/composeResources/values/string.xml
+++ b/composeApp/src/commonMain/composeResources/values/string.xml
@@ -2,7 +2,7 @@
Home
Favourite
- Profile
+ Gemini
Cart
RATING
TYPE
@@ -18,4 +18,5 @@
Category
All
Number Of People
+ Gemini ChatBot
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt
index 316493e..efbcfaa 100644
--- a/composeApp/src/commonMain/kotlin/App.kt
+++ b/composeApp/src/commonMain/kotlin/App.kt
@@ -4,11 +4,10 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.compose.ui.graphics.ImageBitmap
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.CurrentTab
-import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabNavigator
import coil3.ImageLoader
import coil3.PlatformContext
@@ -18,6 +17,7 @@ import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import coil3.request.crossfade
import coil3.util.DebugLogger
+import di.HomeScreenModelProvider
import okio.FileSystem
import theme.TravelAppTheme
import ui.component.BottomMenuBar
@@ -25,20 +25,19 @@ import ui.component.tabs
import ui.screen.CartTab
import ui.screen.FavoriteTab
import ui.screen.HomeTab
-import ui.screen.ProfileTab
-import ui.viewmodel.HomeViewModel
+import ui.screen.GeminiTab
import util.AnimateVisibility
@Composable
-internal fun App(
- viewModel: HomeViewModel = viewModel { HomeViewModel() }
-) {
+internal fun App() {
TravelAppTheme {
setSingletonImageLoaderFactory { context ->
getAsyncImageLoader(context)
}
+ val viewModel = HomeScreenModelProvider.homeScreenModel
+
val bottomNavBarVisibility by viewModel.bottomNavBarVisible.collectAsState()
TabNavigator(HomeTab) {
@@ -57,7 +56,7 @@ internal fun App(
HomeTab -> LocalNavigator.currentOrThrow.push(HomeTab)
FavoriteTab -> LocalNavigator.currentOrThrow.push(FavoriteTab)
CartTab -> LocalNavigator.currentOrThrow.push(CartTab)
- ProfileTab -> LocalNavigator.currentOrThrow.push(ProfileTab)
+ GeminiTab -> LocalNavigator.currentOrThrow.push(GeminiTab)
}
}
}
@@ -81,4 +80,6 @@ fun newDiskCache(): DiskCache {
return DiskCache.Builder().directory(FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "image_cache")
.maxSizeBytes(1024L * 1024 * 1024) // 512MB
.build()
-}
\ No newline at end of file
+}
+
+expect fun ByteArray.toComposeImageBitmap(): ImageBitmap
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/GeminiApi.kt b/composeApp/src/commonMain/kotlin/data/GeminiApi.kt
new file mode 100644
index 0000000..aa39427
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/GeminiApi.kt
@@ -0,0 +1,47 @@
+package data
+
+import com.travel.buddy.BuildKonfig
+import dev.shreyaspatil.ai.client.generativeai.GenerativeModel
+import dev.shreyaspatil.ai.client.generativeai.type.GenerateContentResponse
+import dev.shreyaspatil.ai.client.generativeai.type.PlatformImage
+import dev.shreyaspatil.ai.client.generativeai.type.content
+import kotlinx.coroutines.flow.Flow
+import kotlin.io.encoding.ExperimentalEncodingApi
+
+class GeminiApi {
+ companion object {
+ const val PROMPT_GENERATE_UI = "Act as an Android app developer. " +
+ "For the image provided, use Jetpack Compose to build the screen so that " +
+ "the Compose Preview is as close to this image as possible. Also make sure " +
+ "to include imports and use Material3. Only give code part without any extra " +
+ "text or description neither at start or end, your response should contain " +
+ "only code without any explanation."
+ }
+
+
+ private val apiKey = BuildKonfig.GEMINI_API_KEY
+
+
+ val generativeVisionModel = GenerativeModel(
+ modelName = "gemini-1.5-flash",
+ apiKey = apiKey
+ )
+
+ val generativeModel = GenerativeModel(
+ modelName = "gemini-pro",
+ apiKey = apiKey
+ )
+
+ fun generateContent(prompt: String): Flow {
+ return generativeModel.generateContentStream(prompt)
+ }
+
+ @OptIn(ExperimentalEncodingApi::class)
+ fun generateContent(prompt: String, imageData: ByteArray): Flow {
+ val content = content {
+ image(PlatformImage(imageData))
+ text(prompt)
+ }
+ return generativeVisionModel.generateContentStream(content)
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/di/Koin.kt b/composeApp/src/commonMain/kotlin/di/Koin.kt
index b427bbd..aad0fe5 100755
--- a/composeApp/src/commonMain/kotlin/di/Koin.kt
+++ b/composeApp/src/commonMain/kotlin/di/Koin.kt
@@ -1,5 +1,7 @@
package di
+import ui.viewmodel.HomeScreenModel
+
//import org.koin.core.context.startKoin
//import org.koin.dsl.module
//
@@ -14,3 +16,9 @@ package di
// )
// }
//}
+
+object HomeScreenModelProvider {
+ val homeScreenModel: HomeScreenModel by lazy {
+ HomeScreenModel()
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/model/ChatMessage.kt b/composeApp/src/commonMain/kotlin/model/ChatMessage.kt
new file mode 100644
index 0000000..29e29bc
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/model/ChatMessage.kt
@@ -0,0 +1,3 @@
+package model
+
+data class ChatMessage(val text: String, val isSender: Boolean)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/theme/Color.kt b/composeApp/src/commonMain/kotlin/theme/Color.kt
index 8bf1bf6..be87f6f 100644
--- a/composeApp/src/commonMain/kotlin/theme/Color.kt
+++ b/composeApp/src/commonMain/kotlin/theme/Color.kt
@@ -18,4 +18,7 @@ val ReviewBodyBg = Color(0xFFF6F8FA)
val Yellow = Color(0xFFF8E545)
val Red = Color(0xFFFF2828)
val CategoryChipBg = Color(0xFF4D5652)
-val BorderColor = Color(0xFFAFAFAF)
+val BorderColor = Color(0xFFDDDDDD) // Light grey
+val LinkColor = Color(0xFF0000FF) // Blue
+val CodeBackground = Color(0xFFf9f9f9)
+
diff --git a/composeApp/src/commonMain/kotlin/ui/component/BottomNavigationBar.kt b/composeApp/src/commonMain/kotlin/ui/component/BottomNavigationBar.kt
index 1f542f3..29271f7 100644
--- a/composeApp/src/commonMain/kotlin/ui/component/BottomNavigationBar.kt
+++ b/composeApp/src/commonMain/kotlin/ui/component/BottomNavigationBar.kt
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -40,7 +39,7 @@ import theme.SecondTextColor
import ui.screen.CartTab
import ui.screen.FavoriteTab
import ui.screen.HomeTab
-import ui.screen.ProfileTab
+import ui.screen.GeminiTab
interface Tabx: Tab {
fun defaultTitle(): StringResource
@@ -51,7 +50,7 @@ val tabs = arrayListOf().apply {
add(HomeTab)
add(FavoriteTab)
add(CartTab)
- add(ProfileTab)
+ add(GeminiTab)
}
@Composable
diff --git a/composeApp/src/commonMain/kotlin/ui/component/Shimmer.kt b/composeApp/src/commonMain/kotlin/ui/component/Shimmer.kt
new file mode 100644
index 0000000..c3925b0
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/component/Shimmer.kt
@@ -0,0 +1,82 @@
+package ui.component
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import theme.PrimaryColor
+
+val ShimmerColorShades = listOf(
+ Color.LightGray.copy(0.9f),
+ PrimaryColor.copy(0.2f),
+ Color.LightGray.copy(0.9f)
+)
+@Composable
+fun ShimmerAnimation(
+) {
+ /*
+ Create InfiniteTransition
+ which holds child animation like [Transition]
+ animations start running as soon as they enter
+ the composition and do not stop unless they are removed
+ */
+ val transition = rememberInfiniteTransition()
+ val translateAnim by transition.animateFloat(
+ /*
+ Specify animation positions,
+ initial Values 0F means it
+ starts from 0 position
+ */
+ initialValue = 0f,
+ targetValue = 1000f,
+ animationSpec = infiniteRepeatable(
+
+
+ // Tween Animates between values over specified [durationMillis]
+ tween(durationMillis = 1200, easing = FastOutSlowInEasing),
+ RepeatMode.Reverse
+ )
+ )
+
+ /*
+ Create a gradient using the list of colors
+ Use Linear Gradient for animating in any direction according to requirement
+ start=specifies the position to start with in cartesian like system Offset(10f,10f) means x(10,0) , y(0,10)
+ end = Animate the end position to give the shimmer effect using the transition created above
+ */
+ val brush = Brush.linearGradient(
+ colors = ShimmerColorShades,
+ start = Offset(10f, 10f),
+ end = Offset(translateAnim, translateAnim)
+ )
+
+ ShimmerItem(brush = brush)
+}
+
+@Composable
+fun ShimmerItem(
+ brush: Brush
+) {
+ Spacer(
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .fillMaxWidth()
+ .size(20.dp)
+ .background(brush = brush, shape = RoundedCornerShape(8.dp))
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/screen/ArticleDetailScreen.kt b/composeApp/src/commonMain/kotlin/ui/screen/ArticleDetailScreen.kt
index d7b3f42..d640ce4 100644
--- a/composeApp/src/commonMain/kotlin/ui/screen/ArticleDetailScreen.kt
+++ b/composeApp/src/commonMain/kotlin/ui/screen/ArticleDetailScreen.kt
@@ -1,17 +1,12 @@
package ui.screen
import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@@ -20,34 +15,31 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import di.HomeScreenModelProvider
import model.Article
-import model.Destination
import theme.Black
-import theme.BorderColor
import theme.White
-import ui.component.ChildLayout
-import ui.component.VerticalScrollLayout
import ui.component.article.ArticleBodyHeader
import ui.component.article.ArticleDescription
-import ui.component.article.ArticleDestination
import ui.component.article.ArticleHeader
import ui.component.article.ArticleOther
import ui.component.article.ArticlePharagraphs
-import ui.viewmodel.HomeViewModel
+import ui.viewmodel.HomeScreenModel
import util.BOTTOM_NAV_SPACE
data class ArticleDetailScreen(val article: Article) : Screen {
@Composable
override fun Content() {
+ val screenModel = HomeScreenModelProvider.homeScreenModel
val navigator = LocalNavigator.currentOrThrow
- ArticleDetailScreenView(navigator, article)
+ ArticleDetailScreenView(navigator = navigator, article = article, viewModel = screenModel)
}
}
@@ -55,12 +47,11 @@ data class ArticleDetailScreen(val article: Article) : Screen {
fun ArticleDetailScreenView(
navigator: Navigator,
article: Article,
- viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel { HomeViewModel() },
+ viewModel: HomeScreenModel,
) {
val rememberThumbnail = remember { mutableStateOf(article.thumbnail) }
Column(
modifier = Modifier
- .padding(bottom = BOTTOM_NAV_SPACE)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
@@ -79,7 +70,7 @@ fun ArticleDetailScreenView(
//viewModel.removeFavorite(it)
},
updateBottomNavBarVisible = {
- //viewModel.setBottomNavBarVisible(true)
+ viewModel.setBottomNavBarVisible(true)
}
)
contentSection(article) {
diff --git a/composeApp/src/commonMain/kotlin/ui/screen/CartTab.kt b/composeApp/src/commonMain/kotlin/ui/screen/CartTab.kt
index 98496d6..333f4c1 100644
--- a/composeApp/src/commonMain/kotlin/ui/screen/CartTab.kt
+++ b/composeApp/src/commonMain/kotlin/ui/screen/CartTab.kt
@@ -16,10 +16,12 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.TabOptions
+import di.HomeScreenModelProvider
import model.Destination
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
@@ -32,7 +34,7 @@ import travelbuddy.composeapp.generated.resources.menu_cart
import ui.component.CartCard
import ui.component.LoadItemAfterSafeCast
import ui.component.Tabx
-import ui.viewmodel.HomeViewModel
+import ui.viewmodel.HomeScreenModel
import util.BOTTOM_NAV_SPACE
data object CartTab : Tabx {
@@ -56,15 +58,16 @@ data object CartTab : Tabx {
@Composable
override fun Content() {
+ val screenModel = HomeScreenModelProvider.homeScreenModel
val navigator = LocalNavigator.currentOrThrow
- CartScreenView(navigator)
+ CartScreenView(navigator = navigator, viewModel = screenModel)
}
}
@Composable
fun CartScreenView(
navigator: Navigator,
- viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel { HomeViewModel() },
+ viewModel: HomeScreenModel,
) {
val cartItems by viewModel.cartItems.collectAsState()
@@ -78,7 +81,7 @@ fun CartScreenView(
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 36.dp),
text = stringResource(Res.string.cart_tab),
color = TextColor,
- style = MaterialTheme.typography.bodySmall,
+ style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
}
diff --git a/composeApp/src/commonMain/kotlin/ui/screen/DestinationDetailScreen.kt b/composeApp/src/commonMain/kotlin/ui/screen/DestinationDetailScreen.kt
index 341ded5..67b9510 100644
--- a/composeApp/src/commonMain/kotlin/ui/screen/DestinationDetailScreen.kt
+++ b/composeApp/src/commonMain/kotlin/ui/screen/DestinationDetailScreen.kt
@@ -1,10 +1,17 @@
package ui.screen
-import androidx.compose.foundation.Image
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandIn
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -20,14 +27,16 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
@@ -40,21 +49,24 @@ import androidx.compose.ui.text.toUpperCase
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
+import di.HomeScreenModelProvider
import model.Destination
+import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import theme.PrimaryColor
-import theme.ReviewBodyBg
import theme.White
import theme.SecondTextColor
import theme.TextColor
-import theme.ThirdTextColor
-import theme.Yellow
import travelbuddy.composeapp.generated.resources.Res
+import travelbuddy.composeapp.generated.resources.arrow_forward
import travelbuddy.composeapp.generated.resources.choose_date
import travelbuddy.composeapp.generated.resources.choose_meeting_point
import travelbuddy.composeapp.generated.resources.ci_location
@@ -62,7 +74,6 @@ import travelbuddy.composeapp.generated.resources.estimation
import travelbuddy.composeapp.generated.resources.facilities
import travelbuddy.composeapp.generated.resources.preview
import travelbuddy.composeapp.generated.resources.ratting
-import travelbuddy.composeapp.generated.resources.star
import travelbuddy.composeapp.generated.resources.type
import travelbuddy.composeapp.generated.resources.via
import ui.component.DestinationDetailChipItem
@@ -73,15 +84,16 @@ import ui.component.DestinationDetailSubItemDivider
import ui.component.DestinationDetailSubItemRatting
import ui.component.PrimaryButton
import ui.component.destinationDetailHeader
-import ui.viewmodel.HomeViewModel
+import ui.viewmodel.HomeScreenModel
import util.BOTTOM_NAV_SPACE
import util.ImageItem
data class DestinationDetailScreen(val destination: Destination) : Screen {
@Composable
override fun Content() {
+ val screenModel = HomeScreenModelProvider.homeScreenModel
val navigator = LocalNavigator.currentOrThrow
- DestinationDetailScreenView(navigator, destination)
+ DestinationDetailScreenView(navigator = navigator, destination = destination, viewModel = screenModel)
}
}
@@ -89,12 +101,11 @@ data class DestinationDetailScreen(val destination: Destination) : Screen {
fun DestinationDetailScreenView(
navigator: Navigator,
destination: Destination,
- viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel { HomeViewModel() },
+ viewModel: HomeScreenModel,
) {
val rememberThumbnail = remember { mutableStateOf(destination.thumbnail) }
Column(
modifier = Modifier
- .padding(bottom = BOTTOM_NAV_SPACE)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
@@ -123,6 +134,15 @@ fun DestinationDetailScreenView(
paddingValues = PaddingValues(start = 25.dp, top = 36.dp, end = 25.dp, bottom = 36.dp),
onClick = { viewModel.addToCart(destination) }
)
+
+ GeminiRoundButton(
+ viewModel = viewModel,
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally),
+ logo = Res.drawable.arrow_forward,
+ destination = destination,
+ navigator = navigator
+ )
}
}
@@ -319,3 +339,64 @@ fun contentSection(destination: Destination, onImageClicked: (String) -> Unit) {
DestinationDetailFacilityItem(destination.facilities)
}
}
+
+@Composable
+fun GeminiRoundButton(
+ viewModel: HomeScreenModel,
+ modifier: Modifier = Modifier,
+ logo: DrawableResource,
+ destination: Destination,
+ navigator: Navigator
+) {
+ var isExpanded by remember { mutableStateOf(true) }
+ val navigateToGemini by viewModel.navigateToGemini.collectAsState()
+ if (navigateToGemini.first) {
+ LocalNavigator.current?.pop()
+ LocalTabNavigator.current.current = GeminiTab
+ }
+
+ // Animate width change
+ val buttonWidth by animateDpAsState(
+ targetValue = if (isExpanded) 220.dp else 100.dp,
+ animationSpec = tween(durationMillis = 300)
+ )
+
+ BoxWithConstraints(
+ modifier = modifier
+ .padding(end = 16.dp, bottom = 36.dp)
+ .width(buttonWidth)
+ .background(color = PrimaryColor, shape = RoundedCornerShape(8.dp))
+ .clickable {
+ if (isExpanded) {
+ viewModel.navigateToGimini(Pair(true, destination))
+ }
+ isExpanded = !isExpanded
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Row(
+ modifier = Modifier.padding(vertical = 8.dp, horizontal = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ painter = painterResource(logo),
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ AnimatedVisibility(
+ visible = isExpanded,
+ enter = fadeIn() + expandIn(),
+ exit = fadeOut() + shrinkOut()
+ ) {
+ Text(
+ text = "Explore with Gemini",
+ color = Color.White,
+ style = MaterialTheme.typography.bodyLarge,
+ fontSize = 16.sp
+ )
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/ui/screen/FavoriteTab.kt b/composeApp/src/commonMain/kotlin/ui/screen/FavoriteTab.kt
index 8f8be4d..c5942c7 100644
--- a/composeApp/src/commonMain/kotlin/ui/screen/FavoriteTab.kt
+++ b/composeApp/src/commonMain/kotlin/ui/screen/FavoriteTab.kt
@@ -21,6 +21,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.TabOptions
+import di.HomeScreenModelProvider
import model.Destination
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
@@ -34,7 +35,7 @@ import travelbuddy.composeapp.generated.resources.fav_tab
import travelbuddy.composeapp.generated.resources.favorite_destination
import travelbuddy.composeapp.generated.resources.menu_fav
import ui.component.Tabx
-import ui.viewmodel.HomeViewModel
+import ui.viewmodel.HomeScreenModel
import util.BOTTOM_NAV_SPACE
data object FavoriteTab : Tabx {
@@ -65,8 +66,9 @@ data object FavoriteTab : Tabx {
object FavoriteScreen : Screen {
@Composable
override fun Content() {
+ val screenModel = HomeScreenModelProvider.homeScreenModel
val navigator = LocalNavigator.currentOrThrow
- FavoriteScreenView(navigator)
+ FavoriteScreenView(navigator = navigator, viewModel = screenModel)
}
}
@@ -74,7 +76,7 @@ object FavoriteScreen : Screen {
@Composable
fun FavoriteScreenView(
navigator: Navigator,
- viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel { HomeViewModel() },
+ viewModel: HomeScreenModel,
) {
val favorites by viewModel.favorites.collectAsState()
@@ -88,7 +90,7 @@ fun FavoriteScreenView(
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 36.dp),
text = stringResource(Res.string.favorite_destination),
color = TextColor,
- style = MaterialTheme.typography.bodySmall,
+ style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
}
diff --git a/composeApp/src/commonMain/kotlin/ui/screen/GeminiTab.kt b/composeApp/src/commonMain/kotlin/ui/screen/GeminiTab.kt
new file mode 100644
index 0000000..1a169a4
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/screen/GeminiTab.kt
@@ -0,0 +1,315 @@
+package ui.screen
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.navigator.tab.TabOptions
+import org.jetbrains.compose.resources.DrawableResource
+import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.resources.stringResource
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.core.model.rememberScreenModel
+import com.mikepenz.markdown.m3.Markdown
+import com.mikepenz.markdown.m3.markdownColor
+import data.GeminiApi
+import dev.shreyaspatil.ai.client.generativeai.type.GenerateContentResponse
+import di.HomeScreenModelProvider
+import io.github.vinceglb.filekit.compose.rememberFilePickerLauncher
+import io.github.vinceglb.filekit.core.PickerType
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import theme.BorderColor
+import theme.CodeBackground
+import theme.LinkColor
+import theme.PrimaryColor
+import theme.TextColor
+import toComposeImageBitmap
+import travelbuddy.composeapp.generated.resources.Res
+import travelbuddy.composeapp.generated.resources.chat_bot
+import travelbuddy.composeapp.generated.resources.gemini
+import travelbuddy.composeapp.generated.resources.menu_profile
+import travelbuddy.composeapp.generated.resources.profile_tab
+import ui.component.ShimmerAnimation
+import ui.component.Tabx
+import ui.viewmodel.HomeScreenModel
+import util.BOTTOM_NAV_SPACE
+
+data object GeminiTab : Tabx {
+ override fun defaultTitle(): StringResource = Res.string.profile_tab
+ override fun defaultIcon(): DrawableResource = Res.drawable.chat_bot
+
+ override val options: TabOptions
+ @Composable
+ get() {
+ val title = stringResource(Res.string.profile_tab)
+ val icon = painterResource(Res.drawable.menu_profile)
+
+ return remember {
+ TabOptions(
+ index = 0u,
+ title = title,
+ icon = icon
+ )
+ }
+ }
+
+ @Composable
+ override fun Content() {
+ val screenModel = HomeScreenModelProvider.homeScreenModel
+ val navigator = LocalNavigator.currentOrThrow
+ GeminiScreenView(navigator = navigator, viewModel = screenModel)
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun GeminiScreenView(navigator: Navigator, viewModel: HomeScreenModel){
+ viewModel.setBottomNavBarVisible(true)
+ val api = remember { GeminiApi() }
+ val coroutineScope = rememberCoroutineScope()
+ var selectedImageData by remember { mutableStateOf(null) }
+ var content by remember { mutableStateOf("") }
+ var showProgress by remember { mutableStateOf(false) }
+ var filePath by remember { mutableStateOf("") }
+ var image by remember { mutableStateOf(null) }
+ val navigateToGemini by viewModel.navigateToGemini.collectAsState()
+ var prompt by remember { mutableStateOf("") }
+ val canClearPrompt by remember { derivedStateOf { prompt.isNotBlank() } }
+ if (navigateToGemini.first && navigateToGemini.second != null) {
+ val customPrompt = """
+ As a tourist, I want to explore and learn about a destination. Please provide comprehensive information about the following place: ${navigateToGemini.second?.title}.
+ Include key details such as:
+ - A brief overview of the place
+ - Historical or cultural significance
+ - Popular tourist attractions or landmarks
+ - Best time to visit
+ - Available activities
+ - Images of the destination
+ - Navigation routes or how to reach there from common locations
+ Make the information engaging and easy to understand.
+ """.trimIndent()
+ coroutineScope.launch {
+ println("prompt = $customPrompt")
+ content = ""
+ generateContentAsFlow(api, customPrompt, selectedImageData)
+ .onStart { showProgress = true }
+ .onCompletion {
+ showProgress = false
+ viewModel.navigateToGimini(navigateToGemini.copy(false))
+ }
+ .collect {
+ showProgress = false
+ println("response = ${it.text}")
+ content += it.text
+ }
+ }
+ }
+
+ Surface(modifier = Modifier.fillMaxWidth().padding(bottom = BOTTOM_NAV_SPACE)) {
+ val imagePickerLauncher = rememberFilePickerLauncher(PickerType.Image) { selectedImage ->
+ coroutineScope.launch {
+ val bytes = selectedImage?.readBytes()
+ selectedImageData = bytes
+ image = bytes?.toComposeImageBitmap()
+ filePath = selectedImage?.path ?: ""
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .fillMaxWidth().padding(16.dp)
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 26.dp),
+ text = stringResource(Res.string.gemini),
+ color = TextColor,
+ style = MaterialTheme.typography.headlineMedium,
+ textAlign = TextAlign.Center
+ )
+
+ FlowRow(
+ modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
+ ) {
+ OutlinedTextField(
+ value = navigateToGemini.second?.title ?: prompt,
+ onValueChange = { prompt = it },
+ modifier = Modifier
+ .fillMaxSize()
+ .defaultMinSize(minHeight = 52.dp),
+ label = {
+ Text(
+ text = "Search",
+ color = TextColor,
+ style = MaterialTheme.typography.labelSmall,
+ )
+ },
+ trailingIcon = {
+ if (canClearPrompt) {
+ IconButton(
+ onClick = { prompt = "" }
+ ) {
+ Icon(
+ imageVector = Icons.Default.Clear,
+ contentDescription = "Clear"
+ )
+ }
+ }
+ },
+ colors = TextFieldDefaults.outlinedTextFieldColors(
+ focusedLabelColor = TextColor,
+ unfocusedLabelColor = TextColor,
+ focusedBorderColor = PrimaryColor,
+ unfocusedBorderColor = TextColor,
+ cursorColor = PrimaryColor,
+ focusedTextColor = TextColor,
+ unfocusedTextColor = TextColor
+ )
+ )
+
+ OutlinedButton(
+ onClick = {
+ if (prompt.isNotBlank()) {
+ coroutineScope.launch {
+ println("prompt = $prompt")
+ content = ""
+ generateContentAsFlow(api, prompt, selectedImageData)
+ .onStart { showProgress = true }
+ .onCompletion { showProgress = false }
+ .collect {
+ showProgress = false
+ println("response = ${it.text}")
+ content += it.text
+ }
+ }
+ }
+ },
+ enabled = prompt.isNotBlank(),
+ modifier = Modifier
+ .padding(all = 4.dp)
+ .weight(1f)
+ .align(Alignment.CenterVertically)
+ ) {
+ Text(
+ text = "Submit",
+ color = TextColor,
+ style = MaterialTheme.typography.labelSmall,
+ )
+ }
+
+ OutlinedButton(
+ onClick = { imagePickerLauncher.launch() },
+ modifier = Modifier
+ .padding(all = 4.dp)
+ .weight(1f)
+ .align(Alignment.CenterVertically)
+ ) {
+ Text(
+ text = "Select Image",
+ color = TextColor,
+ style = MaterialTheme.typography.labelSmall,
+ )
+ }
+ }
+
+ Spacer(Modifier.height(16.dp))
+
+ image?.let { imageBitmap ->
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Image(
+ painter = BitmapPainter(imageBitmap),
+ contentDescription = "search_image",
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+
+ Spacer(Modifier.height(16.dp))
+ if (showProgress) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ repeat(5) {
+ ShimmerAnimation()
+ }
+ }
+ }
+ } else {
+ SelectionContainer {
+ Markdown(
+ modifier = Modifier.fillMaxSize(),
+ content = content,
+ colors = markdownColor(
+ text = TextColor,
+ inlineCodeText = PrimaryColor,
+ dividerColor = BorderColor,
+ codeText = PrimaryColor,
+ linkText = LinkColor,
+ codeBackground = CodeBackground,
+ inlineCodeBackground = CodeBackground
+ )
+ )
+ }
+ }
+ }
+ }
+}
+
+fun generateContentAsFlow(
+ api: GeminiApi,
+ prompt: String,
+ imageData: ByteArray? = null
+): Flow = imageData?.let { imageByteArray ->
+ api.generateContent(prompt, imageByteArray)
+} ?: run {
+ api.generateContent(prompt)
+}
diff --git a/composeApp/src/commonMain/kotlin/ui/screen/HomeTab.kt b/composeApp/src/commonMain/kotlin/ui/screen/HomeTab.kt
index e61e24d..7b886a4 100644
--- a/composeApp/src/commonMain/kotlin/ui/screen/HomeTab.kt
+++ b/composeApp/src/commonMain/kotlin/ui/screen/HomeTab.kt
@@ -19,12 +19,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
+import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.TabOptions
import data.FakeArticles
+import di.HomeScreenModelProvider
import model.Destination
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
@@ -48,7 +50,7 @@ import ui.component.destinationSmallItem
import ui.component.homeHeader
import ui.component.loadCategoryItems
import ui.component.loadDestinationLargeItems
-import ui.viewmodel.HomeViewModel
+import ui.viewmodel.HomeScreenModel
import util.BOTTOM_NAV_SPACE
enum class HomeScreenContents {
@@ -91,15 +93,16 @@ data object HomeTab : Tabx {
object HomeScreen : Screen {
@Composable
override fun Content() {
+ val screenModel = HomeScreenModelProvider.homeScreenModel
val navigator = LocalNavigator.currentOrThrow
- HomeScreenView(navigator = navigator)
+ HomeScreenView(navigator = navigator, viewModel = screenModel)
}
}
@Composable
fun HomeScreenView(
- viewModel: HomeViewModel = viewModel { HomeViewModel() },
+ viewModel: HomeScreenModel,
navigator: Navigator
) {
val destinations by viewModel.destinations.collectAsState()
diff --git a/composeApp/src/commonMain/kotlin/ui/screen/ProfileTab.kt b/composeApp/src/commonMain/kotlin/ui/screen/ProfileTab.kt
deleted file mode 100644
index 2d129db..0000000
--- a/composeApp/src/commonMain/kotlin/ui/screen/ProfileTab.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package ui.screen
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import cafe.adriel.voyager.navigator.LocalNavigator
-import cafe.adriel.voyager.navigator.Navigator
-import cafe.adriel.voyager.navigator.currentOrThrow
-import cafe.adriel.voyager.navigator.tab.TabOptions
-import org.jetbrains.compose.resources.DrawableResource
-import org.jetbrains.compose.resources.StringResource
-import org.jetbrains.compose.resources.painterResource
-import org.jetbrains.compose.resources.stringResource
-import theme.TextColor
-import travelbuddy.composeapp.generated.resources.Res
-import travelbuddy.composeapp.generated.resources.menu_profile
-import travelbuddy.composeapp.generated.resources.profile_tab
-import ui.component.Tabx
-import util.BOTTOM_NAV_SPACE
-
-data object ProfileTab : Tabx {
- override fun defaultTitle(): StringResource = Res.string.profile_tab
- override fun defaultIcon(): DrawableResource = Res.drawable.menu_profile
-
- override val options: TabOptions
- @Composable
- get() {
- val title = stringResource(Res.string.profile_tab)
- val icon = painterResource(Res.drawable.menu_profile)
-
- return remember {
- TabOptions(
- index = 0u,
- title = title,
- icon = icon
- )
- }
- }
-
- @Composable
- override fun Content() {
- val navigator = LocalNavigator.currentOrThrow
- ProfileScreenView(navigator)
- }
-}
-
-@Composable
-fun ProfileScreenView(navigator: Navigator){
- Surface(modifier = Modifier.fillMaxWidth().padding(bottom = BOTTOM_NAV_SPACE)) {
- Text(
- modifier = Modifier.wrapContentSize(Alignment.Center),
- text = stringResource(Res.string.profile_tab),
- color = TextColor,
- style = MaterialTheme.typography.bodySmall
- )
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/viewmodel/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/ui/viewmodel/HomeScreenModel.kt
similarity index 82%
rename from composeApp/src/commonMain/kotlin/ui/viewmodel/HomeViewModel.kt
rename to composeApp/src/commonMain/kotlin/ui/viewmodel/HomeScreenModel.kt
index 76f1c4b..45cccd3 100644
--- a/composeApp/src/commonMain/kotlin/ui/viewmodel/HomeViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/ui/viewmodel/HomeScreenModel.kt
@@ -1,6 +1,6 @@
package ui.viewmodel
-import androidx.lifecycle.ViewModel
+import cafe.adriel.voyager.core.model.ScreenModel
import data.FakeCart
import data.FakeCategories
import data.FakeDestinations
@@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.update
import model.Category
import model.Destination
-class HomeViewModel : ViewModel() {
+class HomeScreenModel : ScreenModel {
private val _destinations = MutableStateFlow(FakeDestinations.destinations)
val destinations: StateFlow> = _destinations.asStateFlow()
@@ -28,6 +28,10 @@ class HomeViewModel : ViewModel() {
private val _bottomNavBarVisible = MutableStateFlow(true)
val bottomNavBarVisible: StateFlow = _bottomNavBarVisible.asStateFlow()
+ private val _navigateToGemini = MutableStateFlow>(Pair(false, null))
+ val navigateToGemini: StateFlow> = _navigateToGemini.asStateFlow()
+
+
fun setBottomNavBarVisible(value: Boolean) {
_bottomNavBarVisible.update { value }
}
@@ -57,4 +61,8 @@ class HomeViewModel : ViewModel() {
}
}
+ fun navigateToGimini(value: Pair) {
+ _navigateToGemini.value = value
+ }
+
}
\ No newline at end of file
diff --git a/composeApp/src/desktopMain/kotlin/App.desktop.kt b/composeApp/src/desktopMain/kotlin/App.desktop.kt
index c022c32..05aa78b 100644
--- a/composeApp/src/desktopMain/kotlin/App.desktop.kt
+++ b/composeApp/src/desktopMain/kotlin/App.desktop.kt
@@ -1,2 +1,8 @@
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import org.jetbrains.skia.Image
+
internal actual fun openUrl(url: String?) {
-}
\ No newline at end of file
+}
+
+actual fun ByteArray.toComposeImageBitmap(): ImageBitmap = Image.makeFromEncoded(this).toComposeImageBitmap()
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/App.native.kt b/composeApp/src/iosMain/kotlin/App.native.kt
new file mode 100644
index 0000000..0d639a7
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/App.native.kt
@@ -0,0 +1,7 @@
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import org.jetbrains.skia.Image
+
+actual fun ByteArray.toComposeImageBitmap(): ImageBitmap {
+ return Image.makeFromEncoded(this).toComposeImageBitmap()
+}
\ No newline at end of file
diff --git a/composeApp/src/nativeMain/kotlin/App.native.kt b/composeApp/src/nativeMain/kotlin/main.kt
similarity index 100%
rename from composeApp/src/nativeMain/kotlin/App.native.kt
rename to composeApp/src/nativeMain/kotlin/main.kt
diff --git a/composeApp/src/wasmJsMain/kotlin/App.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/App.wasmJs.kt
index 2534b95..0ee74f5 100644
--- a/composeApp/src/wasmJsMain/kotlin/App.wasmJs.kt
+++ b/composeApp/src/wasmJsMain/kotlin/App.wasmJs.kt
@@ -1,5 +1,12 @@
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toComposeImageBitmap
import kotlinx.browser.window
+import org.jetbrains.skia.Image
internal actual fun openUrl(url: String?) {
url?.let { window.open(it) }
+}
+
+actual fun ByteArray.toComposeImageBitmap(): ImageBitmap {
+ return Image.makeFromEncoded(this).toComposeImageBitmap()
}
\ No newline at end of file
diff --git a/composeApp/src/wasmJsMain/kotlin/main.kt b/composeApp/src/wasmJsMain/kotlin/main.kt
index d2fd60c..b61ad36 100644
--- a/composeApp/src/wasmJsMain/kotlin/main.kt
+++ b/composeApp/src/wasmJsMain/kotlin/main.kt
@@ -1,4 +1,5 @@
import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.window.CanvasBasedWindow
import androidx.compose.ui.window.ComposeViewport
import kotlinx.browser.document
@@ -7,4 +8,4 @@ fun main() {
ComposeViewport(document.body!!) {
App()
}
-}
\ No newline at end of file
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1c6a7bb..971cee7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,13 +3,13 @@ agp = "8.2.2"
android-compileSdk = "34"
android-minSdk = "24"
android-targetSdk = "34"
-androidx-activityCompose = "1.9.0"
+androidx-activityCompose = "1.9.1"
androidx-appcompat = "1.7.0"
androidx-constraintlayout = "2.1.4"
androidx-core-ktx = "1.13.1"
-androidx-espresso-core = "3.5.1"
+androidx-espresso-core = "3.6.1"
androidx-material = "1.12.0"
-androidx-test-junit = "1.1.5"
+androidx-test-junit = "1.2.1"
coil3 = "3.0.0-alpha06"
compose-plugin = "1.6.10"
junit = "4.13.2"
@@ -19,6 +19,12 @@ lifecycleViewmodelCompose = "2.8.0"
voyager = "1.1.0-beta02"
koin = "3.5.6"
kotlinxCoroutinesSwing = "1.8.1"
+buildkonfig = "0.15.1"
+markdownRenderer = "0.16.0"
+filekit = "0.8.0"
+generativeai = "0.5.0-1.0.0-wasm"
+horologist = "0.6.12"
+composeWindowSize = "0.5.0"
[libraries]
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
@@ -26,6 +32,7 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3"
coil-compose-core = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil3" }
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil3" }
coil-mp = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
+generativeai = { module = "dev.shreyaspatil.generativeai:generativeai-google", version.ref = "generativeai" }
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
@@ -45,7 +52,18 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
+horologist-composables = { module = "com.google.android.horologist:horologist-composables", version.ref = "horologist" }
+horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" }
+horologist-ai-ui = { module = "com.google.android.horologist:horologist-ai-ui", version.ref = "horologist" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
+compose-window-size = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "composeWindowSize" }
+
+
+filekit-compose = { module = "io.github.vinceglb:filekit-compose", version.ref = "filekit" }
+
+markdown-renderer = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" }
+markdown-renderer-core = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "markdownRenderer" }
+
lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
@@ -53,10 +71,12 @@ voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voy
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesSwing" }
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
+voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
-kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
\ No newline at end of file
+kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig" }
\ No newline at end of file