From 48ccc1cc0ad61a1d36e6c8957cb94b06e7b78b2b Mon Sep 17 00:00:00 2001 From: "Tewelde, Fethi (F.)" Date: Sun, 8 Dec 2024 23:23:16 +0100 Subject: [PATCH] move detail to different module --- composeApp/build.gradle.kts | 8 +- .../kotlin/com/tewelde/rijksmuseum/App.kt | 4 +- .../com/tewelde/rijksmuseum/di/appModule.kt | 5 +- .../navigation/RijksmuseumNavGraph.kt | 60 +++++--------- .../com/tewelde/rijksmuseum}/theme/Color.kt | 2 +- .../com/tewelde/rijksmuseum}/theme/Theme.kt | 2 +- .../com/tewelde/rijksmuseum}/theme/Type.kt | 2 +- .../designsystem/RijksmuseumDestination.kt | 12 --- feature/arts/build.gradle.kts | 5 -- .../src/androidMain/kotlin/Utils.android.kt | 76 +----------------- feature/arts/src/commonMain/kotlin/Utils.kt | 31 +------- .../rijksmuseum/feature/arts/di/ArtsModule.kt | 13 +-- .../feature/arts/gallery/ArtsScreen.kt | 4 +- .../feature/arts/gallery/CollectionScreen.kt | 4 +- .../feature/arts/navigation/ArtsNavigation.kt | 36 +++++---- .../src/desktopMain/kotlin/Utils.desktop.kt | 51 ------------ feature/arts/src/iosMain/kotlin/Utils.ios.kt | 77 +----------------- .../src/wasmJsMain/kotlin/Utils.wasmJs.kt | 53 +------------ feature/detail/build.gradle.kts | 29 +++++++ .../feature/detail/Utils.android.kt | 76 ++++++++++++++++++ .../detail/di/DetailModule.android.kt} | 4 +- .../rijksmuseum/feature/detail/Utils.kt | 31 ++++++++ .../feature/detail}/components/ArtDetail.kt | 4 +- .../feature/detail}/components/BackButton.kt | 2 +- .../detail}/components/ExpandableText.kt | 2 +- .../feature/detail}/detail/DetailScreen.kt | 22 +++--- .../feature/detail}/detail/DetailViewModel.kt | 12 +-- .../detail}/detail/model/DetailEvent.kt | 2 +- .../feature/detail}/detail/model/State.kt | 2 +- .../feature/detail/di/DetailModule.kt | 15 ++++ .../detail/navigation/ArtsNavigation.kt | 41 ++++++++++ .../feature/detail/Utils.desktop.kt | 53 +++++++++++++ .../detail/di/DetailModule.desktop.kt} | 4 +- .../rijksmuseum/feature/detail/Utils.ios.kt | 79 +++++++++++++++++++ .../feature/detail/di/DetailModule.ios.kt} | 4 +- .../feature/detail/Utils.wasmJs.kt | 53 +++++++++++++ .../feature/detail/di/DetailModule.wasmJs.kt} | 4 +- gradle/libs.versions.toml | 18 ++--- settings.gradle.kts | 3 +- 39 files changed, 481 insertions(+), 424 deletions(-) rename {core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem => composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum}/theme/Color.kt (99%) rename {core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem => composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum}/theme/Theme.kt (99%) rename {core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem => composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum}/theme/Type.kt (97%) delete mode 100644 core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/RijksmuseumDestination.kt create mode 100644 feature/detail/build.gradle.kts create mode 100644 feature/detail/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.android.kt rename feature/{arts/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.wasmJs.kt => detail/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.android.kt} (62%) create mode 100644 feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.kt rename feature/{arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts => detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail}/components/ArtDetail.kt (98%) rename feature/{arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery => detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail}/components/BackButton.kt (94%) rename feature/{arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts => detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail}/components/ExpandableText.kt (98%) rename feature/{arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts => detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail}/detail/DetailScreen.kt (94%) rename feature/{arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts => detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail}/detail/DetailViewModel.kt (95%) rename feature/{arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts => detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail}/detail/model/DetailEvent.kt (88%) rename feature/{arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts => detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail}/detail/model/State.kt (89%) create mode 100644 feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.kt create mode 100644 feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/navigation/ArtsNavigation.kt create mode 100644 feature/detail/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.desktop.kt rename feature/{arts/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.android.kt => detail/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.desktop.kt} (62%) create mode 100644 feature/detail/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.ios.kt rename feature/{arts/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.desktop.kt => detail/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.ios.kt} (62%) create mode 100644 feature/detail/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.wasmJs.kt rename feature/{arts/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.ios.kt => detail/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.wasmJs.kt} (62%) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 51a30f4..0b62307 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget @@ -60,8 +61,8 @@ kotlin { } commonMain.dependencies { - implementation(projects.core.designsystem) implementation(projects.feature.arts) + implementation(projects.feature.detail) implementation(compose.material3) implementation(compose.components.resources) @@ -92,7 +93,10 @@ kotlin { } composeCompiler { - enableStrongSkippingMode = true + featureFlags = setOf( + ComposeFeatureFlag.StrongSkipping, + ComposeFeatureFlag.OptimizeNonSkippingGroups + ) } android { diff --git a/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/App.kt b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/App.kt index 6cbfaf8..e213997 100644 --- a/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/App.kt +++ b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/App.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import coil3.ImageLoader import coil3.PlatformContext -import coil3.annotation.ExperimentalCoilApi import coil3.compose.setSingletonImageLoaderFactory import coil3.disk.DiskCache import coil3.memory.MemoryCache @@ -13,13 +12,12 @@ import coil3.network.ktor3.KtorNetworkFetcherFactory import coil3.request.CachePolicy import coil3.request.crossfade import coil3.util.DebugLogger -import com.tewelde.rijksmuseum.core.designsystem.theme.RijksmuseumTheme import com.tewelde.rijksmuseum.navigation.RijksmuseumNavGraph +import com.tewelde.rijksmuseum.theme.RijksmuseumTheme import okio.FileSystem import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.KoinContext -@OptIn(ExperimentalCoilApi::class) @Composable @Preview fun App(disableDiskCache: Boolean = false) { diff --git a/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/di/appModule.kt b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/di/appModule.kt index 7e24837..16ed7c5 100644 --- a/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/di/appModule.kt +++ b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/di/appModule.kt @@ -5,16 +5,17 @@ import coil3.network.CacheStrategy import coil3.network.NetworkFetcher import coil3.network.ktor3.asNetworkClient import com.tewelde.rijksmuseum.feature.arts.di.artsModule +import com.tewelde.rijksmuseum.feature.detail.di.detailModule import io.ktor.client.HttpClient import org.koin.dsl.module @OptIn(ExperimentalCoilApi::class) val appModule = module { - includes(artsModule) + includes(artsModule, detailModule) single { NetworkFetcher.Factory( networkClient = { get().asNetworkClient() }, - cacheStrategy = { CacheStrategy() }, + cacheStrategy = { CacheStrategy.DEFAULT }, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/navigation/RijksmuseumNavGraph.kt b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/navigation/RijksmuseumNavGraph.kt index b86791b..be4f3a6 100644 --- a/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/navigation/RijksmuseumNavGraph.kt +++ b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/navigation/RijksmuseumNavGraph.kt @@ -4,17 +4,13 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.tewelde.rijksmuseum.core.designsystem.RijksmuseumDestination -import com.tewelde.rijksmuseum.feature.arts.detail.DetailScreenRoute -import com.tewelde.rijksmuseum.feature.arts.detail.DetailViewModel +import com.tewelde.rijksmuseum.feature.arts.navigation.Gallery import com.tewelde.rijksmuseum.feature.arts.navigation.galleryGraph -import org.koin.compose.currentKoinScope +import com.tewelde.rijksmuseum.feature.detail.navigation.detailScreen +import com.tewelde.rijksmuseum.feature.detail.navigation.navigateToDetail /** * Main navigation graph for the Art Gallery Viewer app. @@ -26,43 +22,29 @@ import org.koin.compose.currentKoinScope fun RijksmuseumNavGraph( modifier: Modifier = Modifier, snackbarHostState: SnackbarHostState, - startDestination: RijksmuseumDestination = RijksmuseumDestination.Gallery, + startDestination: Any = Gallery, navController: NavHostController = rememberNavController(), ) { NavHost( modifier = modifier, - startDestination = startDestination.route, + startDestination = startDestination, navController = navController, ) { - galleryGraph(navController) - composable(RijksmuseumDestination.DetailScreen.route) { entry -> - val id = entry.arguments - ?.getString("id") - ?.let(::requireNotNull) - .orEmpty() - val viewModel = koinViewModel() - - DetailScreenRoute( - objectId = id, - viewModel = viewModel, - onBackClick = navController::navigateUp, - snackbarHostState = snackbarHostState, - onShowSnackbar = { message, action, duration -> - snackbarHostState.showSnackbar( - message = message, - actionLabel = action, - duration = duration, - ) == SnackbarResult.ActionPerformed - } - ) - } - } -} - -@Composable -inline fun koinViewModel(): T { - val scope = currentKoinScope() - return viewModel { - scope.get() + galleryGraph( + navController = navController, + onBackClick = navController::navigateUp, + onArtClick = { id -> navController.navigateToDetail(id) } + ) + detailScreen( + onBackClick = navController::navigateUp, + snackbarHostState = snackbarHostState, + onShowSnackbar = { message, action, duration -> + snackbarHostState.showSnackbar( + message = message, + actionLabel = action, + duration = duration, + ) == SnackbarResult.ActionPerformed + } + ) } } \ No newline at end of file diff --git a/core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/theme/Color.kt b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/theme/Color.kt similarity index 99% rename from core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/theme/Color.kt rename to composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/theme/Color.kt index 478c2c1..2a0f9f5 100644 --- a/core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/theme/Color.kt +++ b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/theme/Color.kt @@ -1,4 +1,4 @@ -package com.tewelde.rijksmuseum.core.designsystem.theme +package com.tewelde.rijksmuseum.theme import androidx.compose.ui.graphics.Color diff --git a/core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/theme/Theme.kt similarity index 99% rename from core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/theme/Theme.kt rename to composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/theme/Theme.kt index 62ac613..d71d9b5 100644 --- a/core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/theme/Theme.kt +++ b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.tewelde.rijksmuseum.core.designsystem.theme +package com.tewelde.rijksmuseum.theme import androidx.compose.foundation.isSystemInDarkTheme diff --git a/core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/theme/Type.kt b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/theme/Type.kt similarity index 97% rename from core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/theme/Type.kt rename to composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/theme/Type.kt index da93974..183717c 100644 --- a/core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/theme/Type.kt +++ b/composeApp/src/commonMain/kotlin/com/tewelde/rijksmuseum/theme/Type.kt @@ -1,4 +1,4 @@ -package com.tewelde.rijksmuseum.core.designsystem.theme +package com.tewelde.rijksmuseum.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/RijksmuseumDestination.kt b/core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/RijksmuseumDestination.kt deleted file mode 100644 index e96d309..0000000 --- a/core/designsystem/src/commonMain/kotlin/com/tewelde/rijksmuseum/core/designsystem/RijksmuseumDestination.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tewelde.rijksmuseum.core.designsystem - -/** - * Destinations used in the [RijksmuseumDestination]. - */ -/* app module may be better suited to host the nav destinations */ -sealed class RijksmuseumDestination(val route: String) { - data object Gallery : RijksmuseumDestination("gallery") - data object ArtsScreen : RijksmuseumDestination("arts") - data object DetailScreen : RijksmuseumDestination("detail/{id}") - data object CollectionScreen : RijksmuseumDestination("collection") -} \ No newline at end of file diff --git a/feature/arts/build.gradle.kts b/feature/arts/build.gradle.kts index 60e2e03..c74509b 100644 --- a/feature/arts/build.gradle.kts +++ b/feature/arts/build.gradle.kts @@ -9,20 +9,15 @@ kotlin { commonMain.dependencies { api(projects.core.common) implementation(projects.core.model) - implementation(projects.core.permissions) api(projects.core.domain) - implementation(projects.core.permissions) implementation(projects.core.designsystem) - implementation(libs.koin.compose) implementation(libs.koin.composeVM) implementation(libs.navigation.compose) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.runtimeCompose) - implementation(libs.filekit.core) - implementation(libs.coil.compose) } } diff --git a/feature/arts/src/androidMain/kotlin/Utils.android.kt b/feature/arts/src/androidMain/kotlin/Utils.android.kt index ae0ef34..4b9b50b 100644 --- a/feature/arts/src/androidMain/kotlin/Utils.android.kt +++ b/feature/arts/src/androidMain/kotlin/Utils.android.kt @@ -1,81 +1,7 @@ -import android.content.ContentValues -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.os.Build -import android.os.Environment -import android.provider.MediaStore -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalView import com.tewelde.rijksmuseum.core.model.Art -import com.tewelde.rijksmuseum.resources.Res -import com.tewelde.rijksmuseum.resources.permission_denied -import io.github.vinceglb.filekit.core.FileKitPlatformSettings -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import okio.FileSystem -import org.jetbrains.compose.resources.StringResource -import kotlin.coroutines.coroutineContext - -@Composable -actual fun screenHeight(): Int = LocalView.current.resources.displayMetrics.heightPixels - -@Composable -actual fun screenWidth(): Int = LocalView.current.resources.displayMetrics.widthPixels - - -actual class FileUtil(private val context: Context) { - actual fun filesystem(): FileSystem? = FileSystem.SYSTEM - - actual suspend fun saveFile( - bytes: ByteArray, - baseName: String, - extension: String, - initialDirectory: String?, - platformSettings: FileKitPlatformSettings?, - onFailure: (Throwable) -> Unit, - onSuccess: () -> Unit - ) { - CoroutineScope(coroutineContext).launch(Dispatchers.IO) { - runCatching { - val imageBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - val mContentValues = - ContentValues().apply { - put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) - put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") - put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) - put(MediaStore.Images.Media.DISPLAY_NAME, baseName) - } - - context.contentResolver - .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mContentValues) - .apply { - this?.let { - context.contentResolver.openOutputStream(it)?.let { outStream -> - imageBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream) - } - } - onSuccess() - } - }.onFailure { - it.printStackTrace() - onFailure(it) - } - } - } - - actual suspend fun shouldAskStorageRuntimePermission(): Boolean = - Build.VERSION.SDK_INT < Build.VERSION_CODES.Q -} actual val Art.artUrl: String get() = this.webImage.url actual val minGridSize: Int - get() = 175 - -actual val permissionDeniedMessage: StringResource = Res.string.permission_denied - -actual val web: Boolean - get() = false \ No newline at end of file + get() = 175 \ No newline at end of file diff --git a/feature/arts/src/commonMain/kotlin/Utils.kt b/feature/arts/src/commonMain/kotlin/Utils.kt index 2d5759e..038797f 100644 --- a/feature/arts/src/commonMain/kotlin/Utils.kt +++ b/feature/arts/src/commonMain/kotlin/Utils.kt @@ -1,33 +1,4 @@ -import androidx.compose.runtime.Composable import com.tewelde.rijksmuseum.core.model.Art -import io.github.vinceglb.filekit.core.FileKitPlatformSettings -import okio.FileSystem -import org.jetbrains.compose.resources.StringResource - -@Composable -expect fun screenHeight(): Int - -@Composable -expect fun screenWidth(): Int - -expect val web: Boolean - -expect val Art.artUrl: String - -expect val permissionDeniedMessage: StringResource expect val minGridSize: Int - -expect class FileUtil { - fun filesystem(): FileSystem? - suspend fun saveFile( - bytes: ByteArray, - baseName: String = "file", - extension: String, - initialDirectory: String? = null, - platformSettings: FileKitPlatformSettings? = null, - onFailure: (Throwable) -> Unit, - onSuccess: () -> Unit - ) - suspend fun shouldAskStorageRuntimePermission(): Boolean -} +expect val Art.artUrl: String \ No newline at end of file diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.kt b/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.kt index 28be843..b81bd61 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.kt +++ b/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.kt @@ -1,18 +1,11 @@ package com.tewelde.rijksmuseum.feature.arts.di import com.tewelde.rijksmuseum.core.domain.di.domainModule -import com.tewelde.rijksmuseum.core.permissions.permissionsModule -import com.tewelde.rijksmuseum.feature.arts.detail.DetailViewModel import com.tewelde.rijksmuseum.feature.arts.gallery.GalleryViewModel -import org.koin.compose.viewmodel.dsl.viewModelOf -import org.koin.core.module.Module +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val artsModule = module { - includes(domainModule, platformModule, permissionsModule) + includes(domainModule) viewModelOf(::GalleryViewModel) - viewModelOf(::DetailViewModel) -} - - -expect val platformModule: Module \ No newline at end of file +} \ No newline at end of file diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/ArtsScreen.kt b/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/ArtsScreen.kt index 57f488a..2a2a127 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/ArtsScreen.kt +++ b/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/ArtsScreen.kt @@ -38,9 +38,7 @@ fun ArtsScreenRoute( viewModel: GalleryViewModel, onNavigateToCollection: () -> Unit ) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle( - lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current - ) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() ArtsScreen( uiState = uiState, onNavigateToCollection = onNavigateToCollection diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/CollectionScreen.kt b/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/CollectionScreen.kt index 618bccc..647473a 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/CollectionScreen.kt +++ b/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/CollectionScreen.kt @@ -49,9 +49,7 @@ fun CollectionScreenRoute( onBackClick: () -> Unit, onArtClick: (String) -> Unit, ) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle( - lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current - ) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() CollectionScreen( uiState = uiState, onBackClick = onBackClick, diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/navigation/ArtsNavigation.kt b/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/navigation/ArtsNavigation.kt index 1dedcc8..2d060cc 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/navigation/ArtsNavigation.kt +++ b/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/navigation/ArtsNavigation.kt @@ -9,38 +9,43 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.compose.navigation -import com.tewelde.rijksmuseum.core.designsystem.RijksmuseumDestination import com.tewelde.rijksmuseum.feature.arts.gallery.ArtsScreenRoute import com.tewelde.rijksmuseum.feature.arts.gallery.CollectionScreenRoute import com.tewelde.rijksmuseum.feature.arts.gallery.GalleryViewModel +import kotlinx.serialization.Serializable import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.annotation.KoinExperimentalAPI + +@Serializable +data object Gallery + +@Serializable +internal data object ArtsScreen + +@Serializable +internal data object CollectionScreen /** * Defines the navigation graph for the gallery feature. */ fun NavGraphBuilder.galleryGraph( - navController: NavHostController + navController: NavHostController, + onBackClick: () -> Unit, + onArtClick: (String) -> Unit ) { - navigation( - route = RijksmuseumDestination.Gallery.route, - startDestination = RijksmuseumDestination.ArtsScreen.route + navigation( + startDestination = ArtsScreen ) { - composable(RijksmuseumDestination.ArtsScreen.route) { entry -> + composable { entry -> val viewModel = entry.sharedViewModel(navController) - ArtsScreenRoute(viewModel) { - navController.navigate(RijksmuseumDestination.CollectionScreen.route) - } + ArtsScreenRoute(viewModel = viewModel) { navController.navigate(CollectionScreen) } } - composable(RijksmuseumDestination.CollectionScreen.route) { entry -> + composable { entry -> val viewModel = entry.sharedViewModel(navController) CollectionScreenRoute( viewModel = viewModel, - onBackClick = { navController.navigateUp() }, - onArtClick = { id -> - navController.navigate("detail/$id") - } + onBackClick = onBackClick, + onArtClick = onArtClick ) } } @@ -50,7 +55,6 @@ fun NavGraphBuilder.galleryGraph( * Returns a [ViewModel] scoped to the parent of the current [NavBackStackEntry]. * This is useful when you want to share a ViewModel between multiple destinations in a navigation graph. */ -@OptIn(KoinExperimentalAPI::class) @Composable inline fun NavBackStackEntry.sharedViewModel( navController: NavHostController, diff --git a/feature/arts/src/desktopMain/kotlin/Utils.desktop.kt b/feature/arts/src/desktopMain/kotlin/Utils.desktop.kt index 0e258cd..ed4433f 100644 --- a/feature/arts/src/desktopMain/kotlin/Utils.desktop.kt +++ b/feature/arts/src/desktopMain/kotlin/Utils.desktop.kt @@ -1,58 +1,7 @@ -import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.platform.LocalWindowInfo import com.tewelde.rijksmuseum.core.model.Art -import com.tewelde.rijksmuseum.resources.Res -import com.tewelde.rijksmuseum.resources.permission_denied -import io.github.vinceglb.filekit.core.FileKit -import io.github.vinceglb.filekit.core.FileKitPlatformSettings -import okio.FileSystem -import org.jetbrains.compose.resources.StringResource - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -actual fun screenHeight(): Int = LocalWindowInfo.current.containerSize.height - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -actual fun screenWidth(): Int = LocalWindowInfo.current.containerSize.width - - -actual class FileUtil { - actual fun filesystem(): FileSystem? = FileSystem.SYSTEM - actual suspend fun saveFile( - bytes: ByteArray, - baseName: String, - extension: String, - initialDirectory: String?, - platformSettings: FileKitPlatformSettings?, - onFailure: (Throwable) -> Unit, - onSuccess: () -> Unit - ) { - try { - val file = FileKit.saveFile( - bytes = bytes, - baseName = baseName, - extension = extension, - initialDirectory = initialDirectory, - platformSettings = platformSettings - ) - file?.let { onSuccess() } ?: onFailure(Exception("File not saved")) - } catch (e: Exception) { - onFailure(e) - } - } - - actual suspend fun shouldAskStorageRuntimePermission(): Boolean = false -} actual val Art.artUrl: String get() = this.headerImage?.url ?: this.webImage.url actual val minGridSize: Int get() = 325 - -actual val permissionDeniedMessage: StringResource = Res.string.permission_denied - -actual val web: Boolean - get() = false \ No newline at end of file diff --git a/feature/arts/src/iosMain/kotlin/Utils.ios.kt b/feature/arts/src/iosMain/kotlin/Utils.ios.kt index 99226a4..4b9b50b 100644 --- a/feature/arts/src/iosMain/kotlin/Utils.ios.kt +++ b/feature/arts/src/iosMain/kotlin/Utils.ios.kt @@ -1,82 +1,7 @@ -import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.platform.LocalWindowInfo import com.tewelde.rijksmuseum.core.model.Art -import com.tewelde.rijksmuseum.resources.Res -import com.tewelde.rijksmuseum.resources.permission_denied_ios -import io.github.vinceglb.filekit.core.FileKitPlatformSettings -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.allocArrayOf -import kotlinx.cinterop.memScoped -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import kotlinx.coroutines.launch -import okio.FileSystem -import org.jetbrains.compose.resources.StringResource -import platform.Foundation.NSData -import platform.Foundation.create -import platform.UIKit.UIImage -import platform.UIKit.UIImageWriteToSavedPhotosAlbum -import kotlin.coroutines.coroutineContext - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -actual fun screenHeight(): Int = LocalWindowInfo.current.containerSize.height - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -actual fun screenWidth(): Int = LocalWindowInfo.current.containerSize.width - -actual class FileUtil { - actual fun filesystem(): FileSystem? = FileSystem.SYSTEM - - @OptIn( - ExperimentalForeignApi::class - ) - actual suspend fun saveFile( - bytes: ByteArray, - baseName: String, - extension: String, - initialDirectory: String?, - platformSettings: FileKitPlatformSettings?, - onFailure: (Throwable) -> Unit, - onSuccess: () -> Unit - ) { - CoroutineScope(coroutineContext).launch(Dispatchers.IO) { - runCatching { - val nsData: NSData = memScoped { - NSData.create(bytes = allocArrayOf(bytes), length = bytes.size.toULong()) - } - val imageData = UIImage.imageWithData(data = nsData) - if (imageData != null) { - UIImageWriteToSavedPhotosAlbum( - image = imageData, - completionTarget = null, - completionSelector = null, - contextInfo = null - ) - } else { - onFailure(NullPointerException()) - } - }.onSuccess { - onSuccess() - }.onFailure { - it.printStackTrace() - onFailure(it) - } - } - } - - actual suspend fun shouldAskStorageRuntimePermission(): Boolean = true -} actual val Art.artUrl: String get() = this.webImage.url actual val minGridSize: Int - get() = 175 -actual val permissionDeniedMessage: StringResource = Res.string.permission_denied_ios - -actual val web: Boolean - get() = false \ No newline at end of file + get() = 175 \ No newline at end of file diff --git a/feature/arts/src/wasmJsMain/kotlin/Utils.wasmJs.kt b/feature/arts/src/wasmJsMain/kotlin/Utils.wasmJs.kt index c7bf43e..ecdc618 100644 --- a/feature/arts/src/wasmJsMain/kotlin/Utils.wasmJs.kt +++ b/feature/arts/src/wasmJsMain/kotlin/Utils.wasmJs.kt @@ -1,58 +1,7 @@ -import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.platform.LocalWindowInfo import com.tewelde.rijksmuseum.core.model.Art -import com.tewelde.rijksmuseum.resources.Res -import com.tewelde.rijksmuseum.resources.permission_denied -import io.github.vinceglb.filekit.core.FileKit -import io.github.vinceglb.filekit.core.FileKitPlatformSettings -import okio.FileSystem -import org.jetbrains.compose.resources.StringResource - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -actual fun screenHeight(): Int = LocalWindowInfo.current.containerSize.height - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -actual fun screenWidth(): Int = LocalWindowInfo.current.containerSize.width - -actual class FileUtil { - actual fun filesystem(): FileSystem? = null - actual suspend fun saveFile( - bytes: ByteArray, - baseName: String, - extension: String, - initialDirectory: String?, - platformSettings: FileKitPlatformSettings?, - onFailure: (Throwable) -> Unit, - onSuccess: () -> Unit - ) { - try { - val file = FileKit.saveFile( - bytes = bytes, - baseName = baseName, - extension = extension, - initialDirectory = initialDirectory, - platformSettings = platformSettings - ) - file?.let { onSuccess() } ?: onFailure(Exception("File not saved")) - } catch (e: Exception) { - onFailure(e) - } - } - - actual suspend fun shouldAskStorageRuntimePermission(): Boolean = false - -} actual val Art.artUrl: String get() = this.headerImage?.url ?: this.webImage.url actual val minGridSize: Int - get() = 325 - -actual val permissionDeniedMessage: StringResource = Res.string.permission_denied - -actual val web: Boolean - get() = true \ No newline at end of file + get() = 325 \ No newline at end of file diff --git a/feature/detail/build.gradle.kts b/feature/detail/build.gradle.kts new file mode 100644 index 0000000..ce974a8 --- /dev/null +++ b/feature/detail/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.rijksmuseum.kotlinMultiplatform) + alias(libs.plugins.rijksmuseum.composeMultiplatform) +} + + +kotlin { + sourceSets { + commonMain.dependencies { + api(projects.core.common) + implementation(projects.core.model) + implementation(projects.core.permissions) + implementation(projects.core.domain) + implementation(projects.core.permissions) + implementation(projects.core.designsystem) + + implementation(libs.koin.compose) + implementation(libs.koin.composeVM) + + implementation(libs.navigation.compose) + implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.lifecycle.runtimeCompose) + + implementation(libs.filekit.core) + + implementation(libs.coil.compose) + } + } +} \ No newline at end of file diff --git a/feature/detail/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.android.kt b/feature/detail/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.android.kt new file mode 100644 index 0000000..78f334d --- /dev/null +++ b/feature/detail/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.android.kt @@ -0,0 +1,76 @@ +package com.tewelde.rijksmuseum.feature.detail + +import android.content.ContentValues +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalView +import com.tewelde.rijksmuseum.resources.Res +import com.tewelde.rijksmuseum.resources.permission_denied +import io.github.vinceglb.filekit.core.FileKitPlatformSettings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okio.FileSystem +import org.jetbrains.compose.resources.StringResource +import kotlin.coroutines.coroutineContext + +@Composable +actual fun screenHeight(): Int = LocalView.current.resources.displayMetrics.heightPixels + +@Composable +actual fun screenWidth(): Int = LocalView.current.resources.displayMetrics.widthPixels + + +actual class FileUtil(private val context: Context) { + actual fun filesystem(): FileSystem? = FileSystem.SYSTEM + + actual suspend fun saveFile( + bytes: ByteArray, + baseName: String, + extension: String, + initialDirectory: String?, + platformSettings: FileKitPlatformSettings?, + onFailure: (Throwable) -> Unit, + onSuccess: () -> Unit + ) { + CoroutineScope(coroutineContext).launch(Dispatchers.IO) { + runCatching { + val imageBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + val mContentValues = + ContentValues().apply { + put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) + put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) + put(MediaStore.Images.Media.DISPLAY_NAME, baseName) + } + + context.contentResolver + .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mContentValues) + .apply { + this?.let { + context.contentResolver.openOutputStream(it)?.let { outStream -> + imageBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream) + } + } + onSuccess() + } + }.onFailure { + it.printStackTrace() + onFailure(it) + } + } + } + + actual suspend fun shouldAskStorageRuntimePermission(): Boolean = + Build.VERSION.SDK_INT < Build.VERSION_CODES.Q +} + +actual val permissionDeniedMessage: StringResource = Res.string.permission_denied + +actual val web: Boolean + get() = false \ No newline at end of file diff --git a/feature/arts/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.wasmJs.kt b/feature/detail/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.android.kt similarity index 62% rename from feature/arts/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.wasmJs.kt rename to feature/detail/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.android.kt index 154af63..153da97 100644 --- a/feature/arts/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.wasmJs.kt +++ b/feature/detail/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.android.kt @@ -1,9 +1,9 @@ -package com.tewelde.rijksmuseum.feature.arts.di +package com.tewelde.rijksmuseum.feature.detail.di -import FileUtil import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module +import com.tewelde.rijksmuseum.feature.detail.FileUtil actual val platformModule: Module = module { factoryOf(::FileUtil) diff --git a/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.kt new file mode 100644 index 0000000..b8ab4d4 --- /dev/null +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.kt @@ -0,0 +1,31 @@ +package com.tewelde.rijksmuseum.feature.detail + +import androidx.compose.runtime.Composable +import io.github.vinceglb.filekit.core.FileKitPlatformSettings +import okio.FileSystem +import org.jetbrains.compose.resources.StringResource + +@Composable +expect fun screenHeight(): Int + +@Composable +expect fun screenWidth(): Int + +expect val web: Boolean + +expect val permissionDeniedMessage: StringResource + +expect class FileUtil { + fun filesystem(): FileSystem? + suspend fun saveFile( + bytes: ByteArray, + baseName: String = "file", + extension: String, + initialDirectory: String? = null, + platformSettings: FileKitPlatformSettings? = null, + onFailure: (Throwable) -> Unit, + onSuccess: () -> Unit + ) + + suspend fun shouldAskStorageRuntimePermission(): Boolean +} diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/components/ArtDetail.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/components/ArtDetail.kt similarity index 98% rename from feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/components/ArtDetail.kt rename to feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/components/ArtDetail.kt index facf222..22ff0ca 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/components/ArtDetail.kt +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/components/ArtDetail.kt @@ -1,4 +1,4 @@ -package com.tewelde.rijksmuseum.feature.arts.components +package com.tewelde.rijksmuseum.feature.detail.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background @@ -34,7 +34,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.tewelde.rijksmuseum.core.model.ArtObject -import com.tewelde.rijksmuseum.feature.arts.detail.initials +import com.tewelde.rijksmuseum.feature.detail.detail.initials import com.tewelde.rijksmuseum.resources.Res import com.tewelde.rijksmuseum.resources.color import com.tewelde.rijksmuseum.resources.description diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/components/BackButton.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/components/BackButton.kt similarity index 94% rename from feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/components/BackButton.kt rename to feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/components/BackButton.kt index a29f749..86926d0 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/gallery/components/BackButton.kt +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/components/BackButton.kt @@ -1,4 +1,4 @@ -package com.tewelde.rijksmuseum.feature.arts.gallery.components +package com.tewelde.rijksmuseum.feature.detail.components import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/components/ExpandableText.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/components/ExpandableText.kt similarity index 98% rename from feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/components/ExpandableText.kt rename to feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/components/ExpandableText.kt index 3fcdf48..ac4aa12 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/components/ExpandableText.kt +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/components/ExpandableText.kt @@ -1,4 +1,4 @@ -package com.tewelde.rijksmuseum.feature.arts.components +package com.tewelde.rijksmuseum.feature.detail.components import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/DetailScreen.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/DetailScreen.kt similarity index 94% rename from feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/DetailScreen.kt rename to feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/DetailScreen.kt index e63b4e2..2699359 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/DetailScreen.kt +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/DetailScreen.kt @@ -1,4 +1,4 @@ -package com.tewelde.rijksmuseum.feature.arts.detail +package com.tewelde.rijksmuseum.feature.detail.detail import androidx.annotation.ColorInt import androidx.compose.foundation.background @@ -35,19 +35,19 @@ import coil3.compose.LocalPlatformContext import com.tewelde.rijksmuseum.core.designsystem.component.RijksmuseumError import com.tewelde.rijksmuseum.core.designsystem.component.RijksmuseumLoading import com.tewelde.rijksmuseum.core.designsystem.component.RijksmuseumZoomableImage -import com.tewelde.rijksmuseum.feature.arts.components.ArtDetail -import com.tewelde.rijksmuseum.feature.arts.detail.model.DetailEvent -import com.tewelde.rijksmuseum.feature.arts.detail.model.DetailState -import com.tewelde.rijksmuseum.feature.arts.detail.model.State -import com.tewelde.rijksmuseum.feature.arts.gallery.components.BackButton +import com.tewelde.rijksmuseum.feature.detail.components.ArtDetail +import com.tewelde.rijksmuseum.feature.detail.components.BackButton +import com.tewelde.rijksmuseum.feature.detail.detail.model.DetailEvent +import com.tewelde.rijksmuseum.feature.detail.detail.model.DetailState +import com.tewelde.rijksmuseum.feature.detail.detail.model.State +import com.tewelde.rijksmuseum.feature.detail.permissionDeniedMessage +import com.tewelde.rijksmuseum.feature.detail.screenHeight +import com.tewelde.rijksmuseum.feature.detail.screenWidth import com.tewelde.rijksmuseum.resources.Res import com.tewelde.rijksmuseum.resources.saving_failed import com.tewelde.rijksmuseum.resources.saving_success import com.tewelde.rijksmuseum.resources.settings import org.jetbrains.compose.resources.getString -import permissionDeniedMessage -import screenHeight -import screenWidth @Composable fun DetailScreenRoute( @@ -57,9 +57,7 @@ fun DetailScreenRoute( onShowSnackbar: suspend (String, String?, SnackbarDuration) -> Boolean, snackbarHostState: SnackbarHostState, ) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle( - lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current - ) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { viewModel.onEvent(DetailEvent.LoadDetail(objectId)) } diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/DetailViewModel.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/DetailViewModel.kt similarity index 95% rename from feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/DetailViewModel.kt rename to feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/DetailViewModel.kt index 435d592..53440a0 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/DetailViewModel.kt +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/DetailViewModel.kt @@ -1,6 +1,6 @@ -package com.tewelde.rijksmuseum.feature.arts.detail +package com.tewelde.rijksmuseum.feature.detail.detail -import FileUtil +import com.tewelde.rijksmuseum.feature.detail.FileUtil import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger @@ -14,9 +14,9 @@ import com.tewelde.rijksmuseum.core.permissions.model.Permission import com.tewelde.rijksmuseum.core.permissions.service.PermissionsService import com.tewelde.rijksmuseum.core.permissions.util.DeniedAlwaysException import com.tewelde.rijksmuseum.core.permissions.util.DeniedException -import com.tewelde.rijksmuseum.feature.arts.detail.model.DetailEvent -import com.tewelde.rijksmuseum.feature.arts.detail.model.DetailState -import com.tewelde.rijksmuseum.feature.arts.detail.model.State +import com.tewelde.rijksmuseum.feature.detail.detail.model.DetailEvent +import com.tewelde.rijksmuseum.feature.detail.detail.model.DetailState +import com.tewelde.rijksmuseum.feature.detail.detail.model.State import io.ktor.utils.io.toByteArray import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import web +import com.tewelde.rijksmuseum.feature.detail.web class DetailViewModel( val getArtDetail: GetArtDetailUseCase, diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/model/DetailEvent.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/model/DetailEvent.kt similarity index 88% rename from feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/model/DetailEvent.kt rename to feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/model/DetailEvent.kt index fe2fad5..145cb61 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/model/DetailEvent.kt +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/model/DetailEvent.kt @@ -1,4 +1,4 @@ -package com.tewelde.rijksmuseum.feature.arts.detail.model +package com.tewelde.rijksmuseum.feature.detail.detail.model import coil3.PlatformContext diff --git a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/model/State.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/model/State.kt similarity index 89% rename from feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/model/State.kt rename to feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/model/State.kt index 915b957..46f74a7 100644 --- a/feature/arts/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/arts/detail/model/State.kt +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/detail/model/State.kt @@ -1,4 +1,4 @@ -package com.tewelde.rijksmuseum.feature.arts.detail.model +package com.tewelde.rijksmuseum.feature.detail.detail.model import com.tewelde.rijksmuseum.core.model.ArtObject diff --git a/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.kt new file mode 100644 index 0000000..9019cf5 --- /dev/null +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.kt @@ -0,0 +1,15 @@ +package com.tewelde.rijksmuseum.feature.detail.di + +import com.tewelde.rijksmuseum.core.domain.di.domainModule +import com.tewelde.rijksmuseum.core.permissions.permissionsModule +import com.tewelde.rijksmuseum.feature.detail.detail.DetailViewModel +import org.koin.core.module.Module +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val detailModule = module { + includes(domainModule, platformModule, permissionsModule) + viewModelOf(::DetailViewModel) +} + +expect val platformModule: Module \ No newline at end of file diff --git a/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/navigation/ArtsNavigation.kt b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/navigation/ArtsNavigation.kt new file mode 100644 index 0000000..c9e714a --- /dev/null +++ b/feature/detail/src/commonMain/kotlin/com/tewelde/rijksmuseum/feature/detail/navigation/ArtsNavigation.kt @@ -0,0 +1,41 @@ +package com.tewelde.rijksmuseum.feature.detail.navigation + +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.tewelde.rijksmuseum.feature.detail.detail.DetailViewModel +import com.tewelde.rijksmuseum.feature.detail.detail.DetailScreenRoute +import kotlinx.serialization.Serializable +import org.koin.compose.viewmodel.koinViewModel + +@Serializable +internal class DetailScreen(val id: String) + +fun NavController.navigateToDetail( + id: String, + navOptions: NavOptions? = null +) = + navigate(route = DetailScreen(id), navOptions) + +fun NavGraphBuilder.detailScreen( + snackbarHostState: SnackbarHostState, + onBackClick: () -> Unit, + onShowSnackbar: suspend (String, String?, SnackbarDuration) -> Boolean, +) { + composable { entry -> + val viewModel = koinViewModel() + val id = entry.toRoute().id + + DetailScreenRoute( + objectId = id, + viewModel = viewModel, + onBackClick = onBackClick, + onShowSnackbar = onShowSnackbar, + snackbarHostState = snackbarHostState + ) + } +} \ No newline at end of file diff --git a/feature/detail/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.desktop.kt b/feature/detail/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.desktop.kt new file mode 100644 index 0000000..298898e --- /dev/null +++ b/feature/detail/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.desktop.kt @@ -0,0 +1,53 @@ +package com.tewelde.rijksmuseum.feature.detail + +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalWindowInfo +import com.tewelde.rijksmuseum.resources.Res +import com.tewelde.rijksmuseum.resources.permission_denied +import io.github.vinceglb.filekit.core.FileKit +import io.github.vinceglb.filekit.core.FileKitPlatformSettings +import okio.FileSystem +import org.jetbrains.compose.resources.StringResource + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +actual fun screenHeight(): Int = LocalWindowInfo.current.containerSize.height + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +actual fun screenWidth(): Int = LocalWindowInfo.current.containerSize.width + + +actual class FileUtil { + actual fun filesystem(): FileSystem? = FileSystem.SYSTEM + actual suspend fun saveFile( + bytes: ByteArray, + baseName: String, + extension: String, + initialDirectory: String?, + platformSettings: FileKitPlatformSettings?, + onFailure: (Throwable) -> Unit, + onSuccess: () -> Unit + ) { + try { + val file = FileKit.saveFile( + bytes = bytes, + baseName = baseName, + extension = extension, + initialDirectory = initialDirectory, + platformSettings = platformSettings + ) + file?.let { onSuccess() } ?: onFailure(Exception("File not saved")) + } catch (e: Exception) { + onFailure(e) + } + } + + actual suspend fun shouldAskStorageRuntimePermission(): Boolean = false +} + +actual val permissionDeniedMessage: StringResource = Res.string.permission_denied + +actual val web: Boolean + get() = false \ No newline at end of file diff --git a/feature/arts/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.android.kt b/feature/detail/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.desktop.kt similarity index 62% rename from feature/arts/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.android.kt rename to feature/detail/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.desktop.kt index 154af63..153da97 100644 --- a/feature/arts/src/androidMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.android.kt +++ b/feature/detail/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.desktop.kt @@ -1,9 +1,9 @@ -package com.tewelde.rijksmuseum.feature.arts.di +package com.tewelde.rijksmuseum.feature.detail.di -import FileUtil import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module +import com.tewelde.rijksmuseum.feature.detail.FileUtil actual val platformModule: Module = module { factoryOf(::FileUtil) diff --git a/feature/detail/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.ios.kt b/feature/detail/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.ios.kt new file mode 100644 index 0000000..059a5b4 --- /dev/null +++ b/feature/detail/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.ios.kt @@ -0,0 +1,79 @@ +package com.tewelde.rijksmuseum.feature.detail + +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalWindowInfo +import com.tewelde.rijksmuseum.resources.Res +import com.tewelde.rijksmuseum.resources.permission_denied_ios +import io.github.vinceglb.filekit.core.FileKitPlatformSettings +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.allocArrayOf +import kotlinx.cinterop.memScoped +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.launch +import okio.FileSystem +import org.jetbrains.compose.resources.StringResource +import platform.Foundation.NSData +import platform.Foundation.create +import platform.UIKit.UIImage +import platform.UIKit.UIImageWriteToSavedPhotosAlbum +import kotlin.coroutines.coroutineContext + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +actual fun screenHeight(): Int = LocalWindowInfo.current.containerSize.height + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +actual fun screenWidth(): Int = LocalWindowInfo.current.containerSize.width + +actual class FileUtil { + actual fun filesystem(): FileSystem? = FileSystem.SYSTEM + + @OptIn( + ExperimentalForeignApi::class + ) + actual suspend fun saveFile( + bytes: ByteArray, + baseName: String, + extension: String, + initialDirectory: String?, + platformSettings: FileKitPlatformSettings?, + onFailure: (Throwable) -> Unit, + onSuccess: () -> Unit + ) { + CoroutineScope(coroutineContext).launch(Dispatchers.IO) { + runCatching { + val nsData: NSData = memScoped { + NSData.create(bytes = allocArrayOf(bytes), length = bytes.size.toULong()) + } + val imageData = UIImage.imageWithData(data = nsData) + if (imageData != null) { + UIImageWriteToSavedPhotosAlbum( + image = imageData, + completionTarget = null, + completionSelector = null, + contextInfo = null + ) + } else { + onFailure(NullPointerException()) + } + }.onSuccess { + onSuccess() + }.onFailure { + it.printStackTrace() + onFailure(it) + } + } + } + + actual suspend fun shouldAskStorageRuntimePermission(): Boolean = true +} + +actual val permissionDeniedMessage: StringResource = Res.string.permission_denied_ios + +actual val web: Boolean + get() = false \ No newline at end of file diff --git a/feature/arts/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.desktop.kt b/feature/detail/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.ios.kt similarity index 62% rename from feature/arts/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.desktop.kt rename to feature/detail/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.ios.kt index 154af63..153da97 100644 --- a/feature/arts/src/desktopMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.desktop.kt +++ b/feature/detail/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.ios.kt @@ -1,9 +1,9 @@ -package com.tewelde.rijksmuseum.feature.arts.di +package com.tewelde.rijksmuseum.feature.detail.di -import FileUtil import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module +import com.tewelde.rijksmuseum.feature.detail.FileUtil actual val platformModule: Module = module { factoryOf(::FileUtil) diff --git a/feature/detail/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.wasmJs.kt b/feature/detail/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.wasmJs.kt new file mode 100644 index 0000000..8a06f95 --- /dev/null +++ b/feature/detail/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/detail/Utils.wasmJs.kt @@ -0,0 +1,53 @@ +package com.tewelde.rijksmuseum.feature.detail + +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalWindowInfo +import com.tewelde.rijksmuseum.resources.Res +import com.tewelde.rijksmuseum.resources.permission_denied +import io.github.vinceglb.filekit.core.FileKit +import io.github.vinceglb.filekit.core.FileKitPlatformSettings +import okio.FileSystem +import org.jetbrains.compose.resources.StringResource + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +actual fun screenHeight(): Int = LocalWindowInfo.current.containerSize.height + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +actual fun screenWidth(): Int = LocalWindowInfo.current.containerSize.width + +actual class FileUtil { + actual fun filesystem(): FileSystem? = null + actual suspend fun saveFile( + bytes: ByteArray, + baseName: String, + extension: String, + initialDirectory: String?, + platformSettings: FileKitPlatformSettings?, + onFailure: (Throwable) -> Unit, + onSuccess: () -> Unit + ) { + try { + val file = FileKit.saveFile( + bytes = bytes, + baseName = baseName, + extension = extension, + initialDirectory = initialDirectory, + platformSettings = platformSettings + ) + file?.let { onSuccess() } ?: onFailure(Exception("File not saved")) + } catch (e: Exception) { + onFailure(e) + } + } + + actual suspend fun shouldAskStorageRuntimePermission(): Boolean = false + +} + +actual val permissionDeniedMessage: StringResource = Res.string.permission_denied + +actual val web: Boolean + get() = true \ No newline at end of file diff --git a/feature/arts/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.ios.kt b/feature/detail/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.wasmJs.kt similarity index 62% rename from feature/arts/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.ios.kt rename to feature/detail/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.wasmJs.kt index 154af63..7f83d7c 100644 --- a/feature/arts/src/iosMain/kotlin/com/tewelde/rijksmuseum/feature/arts/di/ArtsModule.ios.kt +++ b/feature/detail/src/wasmJsMain/kotlin/com/tewelde/rijksmuseum/feature/detail/di/DetailModule.wasmJs.kt @@ -1,6 +1,6 @@ -package com.tewelde.rijksmuseum.feature.arts.di +package com.tewelde.rijksmuseum.feature.detail.di -import FileUtil +import com.tewelde.rijksmuseum.feature.detail.FileUtil import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0800ae8..f2cd721 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -agp = "8.7.2" -android-compileSdk = "34" +agp = "8.7.3" +android-compileSdk = "35" android-minSdk = "24" -android-targetSdk = "34" +android-targetSdk = "35" androidx-activityCompose = "1.9.3" androidx-appcompat = "1.7.0" androidx-constraintlayout = "2.2.0" @@ -10,22 +10,22 @@ androidx-core-ktx = "1.15.0" androidx-espresso-core = "3.6.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" -coil = "3.0.0-alpha09" +coil = "3.0.4" compose-plugin = "1.7.1" filekit = "0.8.7" junit = "4.13.2" -kotlin = "2.0.21" +kotlin = "2.1.0" koin = "4.0.0" koinComposeMultiplatform = "4.0.0" -ktor = "3.0.1" +ktor = "3.0.2" lifecycle = "2.8.4" -navigationCompose = "2.7.0-alpha07" +navigationCompose = "2.8.0-alpha11" kotlinx_serialization_json = "1.7.3" coroutines = "1.9.0" modulegraph = "0.10.1" buildConfig = "5.5.0" kermit = "2.0.4" -zoomimageComposeCoil = "1.1.0-beta01" +zoomimageComposeCoil = "1.1.0-rc03" [libraries] androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } @@ -66,7 +66,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } kotlinx_serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx_serialization_json" } -zoomimage-compose-coil = { module = "io.github.panpf.zoomimage:zoomimage-compose-coil", version.ref = "zoomimageComposeCoil" } +zoomimage-compose-coil = { module = "io.github.panpf.zoomimage:zoomimage-compose-coil3", version.ref = "zoomimageComposeCoil" } [bundles] ktor-common = ["ktor-client-core", "ktor-client-json", "ktor-client-logging", "ktor-client-serialization", "ktor-client-content-negotiation", "ktor-serialization-kotlinx-json"] diff --git a/settings.gradle.kts b/settings.gradle.kts index 2a6b681..2eab872 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,4 +41,5 @@ include(":core:model") include(":core:network") include(":core:permissions") -include(":feature:arts") \ No newline at end of file +include(":feature:arts") +include(":feature:detail") \ No newline at end of file