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