diff --git a/README.md b/README.md index 447fd1e..905424d 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ A project to display images from [Unsplash](https://unsplash.com) API using Kotl * To successfully make API calls to [Unsplash](https://unsplash.com) Endpoint, use a valid API Token from Unsplash. * Enter your valid API token [here](./composeApp/src/commonMain/kotlin/env/Env.kt) -## App Screenshots +## Mobile App Screenshots | ![Screenshot 1](./images/screenshot_1.png) | ![Screenshot 2](./images/screenshot_2.png) | |--------------------------------------------|--------------------------------------------| @@ -48,4 +48,9 @@ A project to display images from [Unsplash](https://unsplash.com) API using Kotl | ![Screenshot 3](./images/screenshot_3.png) | ![Screenshot 4](./images/screenshot_4.png) | |--------------------------------------------|--------------------------------------------| +## Desktop App Screenshots + +| ![Desktop Screenshot 1](./images/desktop_screenshot_1.png) | ![Desktop Screenshot 2](./images/desktop_screenshot_2.png) | +|------------------------------------------------------------|------------------------------------------------------------| +
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 2ae285e..526f9f5 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -20,6 +20,8 @@ kotlin { } } + jvm("desktop") + listOf( iosX64(), iosArm64(), @@ -33,6 +35,8 @@ kotlin { sourceSets { + val desktopMain by getting + sourceSets.commonMain { kotlin.srcDir("build/generated/ksp/metadata") } @@ -68,7 +72,12 @@ kotlin { implementation(libs.koin.composeVM) implementation(libs.kotlin.navigation.compose) implementation(libs.nappier.logging) - + implementation(libs.kotlinx.coroutines.core) + implementation(libs.material3.window.size.multiplatform) + } + desktopMain.dependencies { + implementation(compose.desktop.currentOs) + runtimeOnly(libs.kotlinx.coroutines.swing) } } } @@ -110,3 +119,15 @@ android { } } +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "com.tamuno.unsplash.kmp" + packageVersion = "1.0.0" + } + } +} + diff --git a/composeApp/src/androidMain/kotlin/ui/component/PlatformLayoutAlignment.android.kt b/composeApp/src/androidMain/kotlin/ui/component/PlatformLayoutAlignment.android.kt new file mode 100644 index 0000000..0cb11d0 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/ui/component/PlatformLayoutAlignment.android.kt @@ -0,0 +1,8 @@ +package ui.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.ui.Alignment + +actual fun ColumnScope.HomeScreenTitleAlignment(): Alignment.Horizontal { + return Alignment.Start +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ui/component/CollapsingSearchBar.kt b/composeApp/src/commonMain/kotlin/ui/component/CollapsingSearchBar.kt index f27a3d1..98fa36a 100644 --- a/composeApp/src/commonMain/kotlin/ui/component/CollapsingSearchBar.kt +++ b/composeApp/src/commonMain/kotlin/ui/component/CollapsingSearchBar.kt @@ -2,13 +2,16 @@ package ui.component import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -31,7 +34,7 @@ fun CollapsibleSearchBar( minShrinkHeight: Dp = 0.dp, textValue: String? = null, keyboardAction: (() -> Unit)? = null, - textValueChange: ((String) -> Unit)? = null + textValueChange: ((String) -> Unit)? = null, ) { val keyboard = LocalSoftwareKeyboardController.current @@ -63,26 +66,13 @@ fun CollapsibleSearchBar( UnsplashSearchBox( modifier = Modifier - .wrapContentHeight() - .weight(weight = 3f) - .padding(end = 10.dp), + .wrapContentHeight(), textValue = textValue, keyboard = keyboard, keyboardAction = keyboardAction ) { textValueChange?.invoke(it) } - - SearchButton( - modifier = Modifier - .testTag("search_button") - .size(55.dp) - .weight(weight = 0.6f) - - ) { - keyboard?.hide() - keyboardAction?.invoke() - } } } @@ -93,8 +83,8 @@ fun CollapsibleSearchBar( @ExperimentalFoundationApi @ExperimentalComposeUiApi @Composable -private fun PreviewCollapsibleSearchBar(){ - UnsplashKMPTheme{ +private fun PreviewCollapsibleSearchBar() { + UnsplashKMPTheme { CollapsibleSearchBar() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ui/component/PlatformLayoutAlignment.kt b/composeApp/src/commonMain/kotlin/ui/component/PlatformLayoutAlignment.kt new file mode 100644 index 0000000..1e27d50 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ui/component/PlatformLayoutAlignment.kt @@ -0,0 +1,6 @@ +package ui.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.ui.Alignment + +expect fun ColumnScope.HomeScreenTitleAlignment(): Alignment.Horizontal \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ui/component/SearchBox.kt b/composeApp/src/commonMain/kotlin/ui/component/SearchBox.kt index 786f609..e6f3f09 100644 --- a/composeApp/src/commonMain/kotlin/ui/component/SearchBox.kt +++ b/composeApp/src/commonMain/kotlin/ui/component/SearchBox.kt @@ -4,7 +4,12 @@ package ui.component import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -43,19 +48,19 @@ fun UnsplashSearchBox( textValue: String? = null, keyboard: SoftwareKeyboardController? = null, keyboardAction: (() -> Unit)? = null, - textValueChange: ((String) -> Unit)? = null + textValueChange: ((String) -> Unit)? = null, ) { Row( modifier = modifier .background( - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(27.5.dp), color = colorDisabledGray ) .border( width = 1.dp, color = colorGrayDivider, - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(27.5.dp) ) .wrapContentHeight() ) { @@ -106,6 +111,16 @@ fun UnsplashSearchBox( .padding(start = 21.dp) .align(Alignment.CenterVertically) ) + }, + trailingIcon = { + SearchButton( + modifier = Modifier + .testTag("search_button") + .size(55.dp) + ) { + keyboard?.hide() + keyboardAction?.invoke() + } } ) } diff --git a/composeApp/src/commonMain/kotlin/ui/component/SearchButton.kt b/composeApp/src/commonMain/kotlin/ui/component/SearchButton.kt index 79dbd29..65dc51b 100644 --- a/composeApp/src/commonMain/kotlin/ui/component/SearchButton.kt +++ b/composeApp/src/commonMain/kotlin/ui/component/SearchButton.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview -import ui.theme.appDark import ui.theme.appWhite import ui.theme.colorDisabledGray import ui.theme.colorGrayDivider @@ -36,13 +35,13 @@ fun SearchButton( modifier = modifier .background( - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(27.5.dp), color = colorDisabledGray ) .border( width = 1.dp, color = colorGrayDivider, - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(27.5.dp) ) ) { Image( diff --git a/composeApp/src/commonMain/kotlin/ui/component/UnsplashImageList.kt b/composeApp/src/commonMain/kotlin/ui/component/UnsplashImageList.kt index fc65d67..eb80363 100644 --- a/composeApp/src/commonMain/kotlin/ui/component/UnsplashImageList.kt +++ b/composeApp/src/commonMain/kotlin/ui/component/UnsplashImageList.kt @@ -23,6 +23,9 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.material.Text +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -45,6 +48,7 @@ import kotlinx.coroutines.flow.Flow import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview +import ui.theme.appDark import ui.theme.appWhite import unsplashkmp.composeapp.generated.resources.Res import unsplashkmp.composeapp.generated.resources.ic_image_search @@ -83,6 +87,7 @@ fun UnsplashImageList( } } +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @ExperimentalFoundationApi @Composable private fun PhotosList( @@ -93,6 +98,15 @@ private fun PhotosList( onItemClicked: (Photo?) -> Unit, onItemLongClicked: (Photo?) -> Unit, ) { + + val windowSizeClass = calculateWindowSizeClass() + + val gridSize = when (windowSizeClass.widthSizeClass) { + WindowWidthSizeClass.Compact -> 2 + WindowWidthSizeClass.Medium -> 3 + else -> 4 + } + LazyVerticalStaggeredGrid( state = lazyGridState, verticalItemSpacing = 8.dp, @@ -101,7 +115,7 @@ private fun PhotosList( .padding(top = 15.dp) .nestedScroll(nestedScrollConnection) .then(modifier), - columns = StaggeredGridCells.Fixed(2) + columns = StaggeredGridCells.Fixed(gridSize) ) { lazyItems(imageList) { photo -> UnsplashImageStaggered( @@ -130,7 +144,7 @@ fun EmptyListStateComponent( Image( modifier = Modifier .size(100.dp) - .background(color = appWhite, shape = CircleShape), + .background(color = appDark, shape = CircleShape), painter = painterResource(Res.drawable.ic_image_search), contentScale = ContentScale.Inside, contentDescription = null diff --git a/composeApp/src/commonMain/kotlin/ui/screen/Home.kt b/composeApp/src/commonMain/kotlin/ui/screen/Home.kt index e7fa091..1ad65a7 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/Home.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/Home.kt @@ -1,5 +1,6 @@ package ui.screen + import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -9,7 +10,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -32,15 +38,13 @@ import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview import ui.component.ChipComponent import ui.component.CollapsibleSearchBar +import ui.component.HomeScreenTitleAlignment import ui.component.UnsplashImageList import ui.component.getEdgeToEdgeTopPadding import ui.dialog.ImagePreviewDialog import ui.event.HomeScreenEvent - - import ui.state.HomeScreenState import ui.theme.UnsplashKMPTheme -import ui.theme.appDark import ui.theme.appWhite import unsplashkmp.composeapp.generated.resources.Res import unsplashkmp.composeapp.generated.resources.unsplash_images @@ -85,6 +89,7 @@ fun HomeScreen( Spacer(modifier = Modifier.padding(top = 20.dp + getEdgeToEdgeTopPadding())) Text( + modifier = Modifier.align(HomeScreenTitleAlignment()), text = stringResource(Res.string.unsplash_images), color = appWhite, fontSize = 22.sp, diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/main.kt new file mode 100644 index 0000000..50d6b10 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/main.kt @@ -0,0 +1,30 @@ +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import di.initKoin +import java.awt.Dimension + +fun main() { + initKoin() + + application { + + val windowState = rememberWindowState( + size = DpSize(width = 900.dp, height = 950.dp), + position = WindowPosition(Alignment.Center) + ) + + Window( + onCloseRequest = ::exitApplication, + title = "UnsplashKMP", + state = windowState + ) { + window.minimumSize = Dimension(950, 900) + App() + } + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/ui/component/EdgeToEdgePlatform.desktop.kt b/composeApp/src/desktopMain/kotlin/ui/component/EdgeToEdgePlatform.desktop.kt new file mode 100644 index 0000000..7659de7 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/ui/component/EdgeToEdgePlatform.desktop.kt @@ -0,0 +1,8 @@ +package ui.component + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +actual fun getEdgeToEdgeTopPadding(): Dp { + return 0.dp +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/ui/component/PlatformLayoutAlignment.desktop.kt b/composeApp/src/desktopMain/kotlin/ui/component/PlatformLayoutAlignment.desktop.kt new file mode 100644 index 0000000..3d11c89 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/ui/component/PlatformLayoutAlignment.desktop.kt @@ -0,0 +1,8 @@ +package ui.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.ui.Alignment + +actual fun ColumnScope.HomeScreenTitleAlignment(): Alignment.Horizontal { + return Alignment.CenterHorizontally +} \ No newline at end of file diff --git a/composeApp/src/nativeMain/kotlin/ui/component/PlatformLayoutAlignment.native.kt b/composeApp/src/nativeMain/kotlin/ui/component/PlatformLayoutAlignment.native.kt new file mode 100644 index 0000000..0cb11d0 --- /dev/null +++ b/composeApp/src/nativeMain/kotlin/ui/component/PlatformLayoutAlignment.native.kt @@ -0,0 +1,8 @@ +package ui.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.ui.Alignment + +actual fun ColumnScope.HomeScreenTitleAlignment(): Alignment.Horizontal { + return Alignment.Start +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a6ed770..2b4f711 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ androidx-test-junit = "1.1.5" compose-plugin = "1.6.10" junit = "4.13.2" kotlin = "2.0.0" +kotlinxCoroutinesCore = "1.9.0-RC" ktorfit = "2.0.0" ksp = "2.0.0-1.0.21" kotlinSerialization = "1.7.0" @@ -26,6 +27,7 @@ ktor = "2.3.11" koinCompose = "3.6.0-Beta4" koinComposeMultiplatform = "1.2.0-Beta4" kotlin-navigation-compose = "2.7.0-alpha07" +material3WindowSizeClassMultiplatform = "0.5.0" nappier-logging = "2.7.1" @@ -40,6 +42,8 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesCore" } ktorfit = { module = "de.jensklingenberg.ktorfit:ktorfit-lib", version.ref = "ktorfit" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } cashapp-paging-common = { module = "app.cash.paging:paging-common", version.ref = "cashapp-paging" } @@ -52,6 +56,7 @@ kotlin-collection-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collecti colormath = { module = "com.github.ajalt.colormath:colormath", version.ref = "color-math" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidxLifecycle" } kotlin-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "kotlin-navigation-compose" } +material3-window-size-multiplatform = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "material3WindowSizeClassMultiplatform" } nappier-logging = { module = "io.github.aakira:napier", version.ref = "nappier-logging" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } @@ -65,7 +70,6 @@ ktor-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } - [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } diff --git a/images/desktop_screenshot_1.png b/images/desktop_screenshot_1.png new file mode 100644 index 0000000..ea50b6c Binary files /dev/null and b/images/desktop_screenshot_1.png differ diff --git a/images/desktop_screenshot_2.png b/images/desktop_screenshot_2.png new file mode 100644 index 0000000..baf0501 Binary files /dev/null and b/images/desktop_screenshot_2.png differ