From 565d1a40051b9110bf8bc0dab71a8efe8cf6e6e3 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 22 Dec 2023 09:07:02 -0500 Subject: [PATCH 01/11] Adding cache using the database --- .../ui/shared/SharedImageConverter.kt | 23 ++++++----- .../com/kevinschildhorn/fotopresenter/Koin.kt | 2 +- .../data/datasources/ImageCacheDataSource.kt | 38 +++++++++++++++++-- .../kevinschildhorn/fotopresenter/Image.sq | 20 ++++++++++ 4 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 shared/src/commonMain/sqldelight/com/kevinschildhorn/fotopresenter/Image.sq diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt index 5646bc5a..275475aa 100644 --- a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt @@ -5,7 +5,8 @@ import android.graphics.BitmapFactory import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.asImageBitmap -import java.nio.ByteBuffer +import java.io.ByteArrayOutputStream + actual object SharedImageConverter { actual fun convertBytes(byteArray: ByteArray): ImageBitmap { @@ -14,16 +15,20 @@ actual object SharedImageConverter { } actual fun convertImage(imageBitmap: ImageBitmap): ByteArray { - return imageBitmap.asAndroidBitmap().convertToByteArray() + val bitmap = imageBitmap.asAndroidBitmap() + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + val byteArray = stream.toByteArray() + return byteArray } private fun Bitmap.convertToByteArray(): ByteArray { - val size = this.byteCount - val buffer = ByteBuffer.allocate(size) - val bytes = ByteArray(size) - this.copyPixelsToBuffer(buffer) - buffer.rewind() - buffer.get(bytes) - return bytes + val stream = ByteArrayOutputStream() + this.compress(Bitmap.CompressFormat.PNG, 100, stream) + val byteArray = stream.toByteArray() + if (this != null && !this.isRecycled()) { + this.recycle(); + } + return byteArray } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt index 0769a39f..b0637d16 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt @@ -41,7 +41,7 @@ val commonModule = single { DirectoryRepository(get()) } single { ImageRemoteDataSource(get()) } single { ImageRepository(get()) } - single { ImageCacheDataSource(get()) } + single { ImageCacheDataSource(get(), get(), baseLogger.withTag("ImageCacheDataSource")) } single { PlaylistDataSource(get(), baseLogger.withTag("PlaylistDataSource")) } single { PlaylistRepository(get()) } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt index 8ad60bbb..b556d6f1 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt @@ -1,17 +1,49 @@ package com.kevinschildhorn.fotopresenter.data.datasources import androidx.compose.ui.graphics.ImageBitmap +import app.cash.sqldelight.db.SqlDriver +import co.touchlab.kermit.Logger +import com.kevinschildhorn.fotopresenter.DiscCache +import com.kevinschildhorn.fotopresenter.PlaylistDatabase import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails import com.kevinschildhorn.fotopresenter.ui.shared.CacheInterface +import com.kevinschildhorn.fotopresenter.ui.shared.SharedImageConverter -class ImageCacheDataSource(private val cache: CacheInterface) { - fun getImage(directory: NetworkDirectoryDetails): ImageBitmap? = cache.getImage(directory.cacheId) +class ImageCacheDataSource( + private val cache: CacheInterface, + driver: SqlDriver, + private val logger: Logger +) { + private val database = PlaylistDatabase(driver) + + + fun getImage(directory: NetworkDirectoryDetails): ImageBitmap? { + logger.i { "Getting Image from Cache ${directory.cacheId}" } + return try { + val image = database.imageQueries.selectImageByName(directory.cacheId).executeAsOne() + SharedImageConverter.convertBytes(image.image) + } catch (e: Exception) { + logger.e(e) { "Image NOT found" } + + null + } + //return DiscCache.getFile(directory.cacheId) + //return cache.getImage(directory.cacheId) + } fun saveImage( directory: NetworkDirectoryDetails, bitmap: ImageBitmap, ) { - cache.cacheImage(directory.cacheId, bitmap) + logger.i { "Saving Image To Cache ${directory.cacheId}" } + database.imageQueries.insertImage( + directory.cacheId, + SharedImageConverter.convertImage(bitmap) + ) + logger.i { "Image Saved" } + + //cache.cacheImage(directory.cacheId, bitmap) + //DiscCache.storeFile(directory.cacheId, bitmap) } private val NetworkDirectoryDetails.cacheId: String diff --git a/shared/src/commonMain/sqldelight/com/kevinschildhorn/fotopresenter/Image.sq b/shared/src/commonMain/sqldelight/com/kevinschildhorn/fotopresenter/Image.sq new file mode 100644 index 00000000..48370e61 --- /dev/null +++ b/shared/src/commonMain/sqldelight/com/kevinschildhorn/fotopresenter/Image.sq @@ -0,0 +1,20 @@ +CREATE TABLE Image ( + id INTEGER PRIMARY KEY NOT NULL, + path TEXT UNIQUE NOT NULL, + image BLOB NOT NULL +); + + +selectImageByName: +SELECT * +FROM Image +WHERE path = ?; + +insertImage: +INSERT OR REPLACE INTO Image(path, image) +VALUES (?, ?); + +deleteImage: +DELETE +FROM Image +WHERE path = ?; \ No newline at end of file From 7dd53633ec7947605b666aff06b485c14de341d2 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 22 Dec 2023 10:56:56 -0500 Subject: [PATCH 02/11] Updating caching --- gradle.properties | 2 +- shared/build.gradle.kts | 2 +- .../data/datasources/ImageCacheDataSource.kt | 3 -- .../domain/image/RetrieveImageUseCase.kt | 37 ++++++++++++++++--- .../screens/directory/DirectoryViewModel.kt | 3 ++ 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/gradle.properties b/gradle.properties index c8e2fcf7..bd4c947f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlin.mpp.androidSourceSetLayoutVersion=2 android.useAndroidX=true android.compileSdk=34 android.targetSdk=34 -android.minSdk=24 +android.minSdk=22 #Versions kotlin.version=1.9.21 diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 0c8b4def..a9111f51 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -60,7 +60,7 @@ kotlin { dependsOn(commonMain) resources.srcDir("src/commonMain/resources") dependencies { - implementation("com.hierynomus:smbj:0.13.0") + implementation("com.hierynomus:smbj:0.11.5") implementation(compose.uiTooling) implementation("app.cash.sqldelight:sqlite-driver:2.0.1") } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt index b556d6f1..ad99df89 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt @@ -3,7 +3,6 @@ package com.kevinschildhorn.fotopresenter.data.datasources import androidx.compose.ui.graphics.ImageBitmap import app.cash.sqldelight.db.SqlDriver import co.touchlab.kermit.Logger -import com.kevinschildhorn.fotopresenter.DiscCache import com.kevinschildhorn.fotopresenter.PlaylistDatabase import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails import com.kevinschildhorn.fotopresenter.ui.shared.CacheInterface @@ -27,7 +26,6 @@ class ImageCacheDataSource( null } - //return DiscCache.getFile(directory.cacheId) //return cache.getImage(directory.cacheId) } @@ -42,7 +40,6 @@ class ImageCacheDataSource( ) logger.i { "Image Saved" } - //cache.cacheImage(directory.cacheId, bitmap) //DiscCache.storeFile(directory.cacheId, bitmap) } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt index 9a3827af..f697a759 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt @@ -13,6 +13,12 @@ class RetrieveImageUseCase( private val imageCacheDataSource: ImageCacheDataSource, private val logger: Logger, ) { + + private val IMAGE_SIZE_SMALL = 64 + private val IMAGE_SIZE_MEDIUM = 256 + private val IMAGE_SIZE_LARGE = 512 + private val IMAGE_SIZE_EXTRA_LARGE = 1024 + suspend operator fun invoke( directory: ImageDirectory, callback: suspend (State) -> Unit, @@ -20,17 +26,38 @@ class RetrieveImageUseCase( logger.i { "Starting to get Image ${directory.name}" } callback(State.LOADING) + var workingWidth: Int = 0 + var updatedImage: Boolean = false imageCacheDataSource.getImage(directory.details)?.let { logger.i { "Image found in cache, using that" } callback(State.SUCCESS(it)) + workingWidth = it.width } logger.i { "Getting Image Bitmap from File ${directory.name}" } - val smallImageBitmap = downloadAndStoreImage(directory, 64) - callback(smallImageBitmap.asState) - - val largeImageBitmap = downloadAndStoreImage(directory, 256) - callback(largeImageBitmap.asState) + if (workingWidth <= IMAGE_SIZE_SMALL) { + val smallImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_SMALL) + callback(smallImageBitmap.asState) + workingWidth = IMAGE_SIZE_SMALL + updatedImage = true + } + if (workingWidth <= IMAGE_SIZE_MEDIUM) { + val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_MEDIUM) + callback(mediumImageBitmap.asState) + workingWidth = IMAGE_SIZE_MEDIUM + updatedImage = true + } + if (workingWidth <= IMAGE_SIZE_LARGE) { + val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_LARGE) + callback(mediumImageBitmap.asState) + workingWidth = IMAGE_SIZE_LARGE + updatedImage = true + } + if (workingWidth <= IMAGE_SIZE_EXTRA_LARGE || !updatedImage) { + val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_EXTRA_LARGE) + callback(mediumImageBitmap.asState) + workingWidth = IMAGE_SIZE_EXTRA_LARGE + } } private fun downloadAndStoreImage( diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt index b8df0360..0c50b6dd 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt @@ -25,6 +25,7 @@ import com.kevinschildhorn.fotopresenter.ui.screens.common.ImageViewModel import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel import com.kevinschildhorn.fotopresenter.ui.shared.ViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -45,6 +46,8 @@ class DirectoryViewModel( private val _directoryContentsState = MutableStateFlow(DirectoryContents()) + private val jobs: List = emptyList() + private val currentPath: String get() = uiState.value.currentPath From 2c3a6b3d9bab60ddf2b7a952ba7fa408bb9bce56 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 22 Dec 2023 19:31:45 -0500 Subject: [PATCH 03/11] Adding sorting, cancelling logs and more --- shared/build.gradle.kts | 2 +- .../ui/compose/CommonPreviews.kt | 15 ++++ .../com/kevinschildhorn/fotopresenter/Koin.kt | 2 +- .../fotopresenter/data/Directory.kt | 16 ++++ .../data/datasources/DirectoryDataSource.kt | 13 +++- .../data/network/NetworkDirectoryDetails.kt | 3 + .../data/network/NetworkHandler.kt | 2 + .../fotopresenter/ui/SortingType.kt | 8 ++ .../ui/screens/common/ImageViewModel.kt | 12 +++ .../common/composables/FilterDialog.kt | 56 ++++++++++++++ .../ui/screens/directory/DirectoryAtoms.kt | 5 ++ .../ui/screens/directory/DirectoryScreen.kt | 47 +++++++++--- .../screens/directory/DirectoryScreenState.kt | 11 +++ .../screens/directory/DirectoryViewModel.kt | 73 ++++++++++++++----- ...nuButton.kt => DirectoryTitleBarButton.kt} | 10 +-- .../ui/screens/login/LoginViewModel.kt | 8 +- .../fotopresenter/data/network/SMBJHandler.kt | 26 ++++--- .../network/SMBJNetworkDirectoryDetails.kt | 3 + 18 files changed, 261 insertions(+), 51 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SortingType.kt create mode 100644 shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/composables/FilterDialog.kt rename shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/navrail/{NavigationRailMenuButton.kt => DirectoryTitleBarButton.kt} (74%) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index a9111f51..c97616e8 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -41,7 +41,7 @@ kotlin { implementation("co.touchlab:kermit:1.2.2") implementation("co.touchlab:kermit-koin:1.2.2") implementation("com.russhwolf:multiplatform-settings:1.0.0") - + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") api("dev.icerock.moko:resources:0.23.0") api("dev.icerock.moko:resources-compose:0.23.0") // for compose multiplatform } diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/CommonPreviews.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/CommonPreviews.kt index 87e6c7bc..206c7312 100644 --- a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/CommonPreviews.kt +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/CommonPreviews.kt @@ -8,6 +8,7 @@ import com.kevinschildhorn.fotopresenter.ui.screens.common.ActionSheetContext import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.ActionSheet import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.ButtonState import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.ConfirmationDialog +import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.FilterDialog import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.PrimaryTextButton import com.kevinschildhorn.fotopresenter.ui.screens.playlist.composables.TextEntryDialog @@ -64,3 +65,17 @@ fun TextConfirmationDialogPreview() { ) } +@Preview +@Composable +fun FilterDialogPreview() { + FilterDialog( + "Hello", + { + + }, + { + + }, + ) +} + diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt index b0637d16..f3a9f259 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt @@ -37,7 +37,7 @@ val commonModule = single { SharedCache } single { CredentialsDataSource(get()) } single { CredentialsRepository(get()) } - single { DirectoryDataSource(get()) } + single { DirectoryDataSource(get(), baseLogger.withTag("DirectoryDataSource")) } single { DirectoryRepository(get()) } single { ImageRemoteDataSource(get()) } single { ImageRepository(get()) } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/Directory.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/Directory.kt index fc9174cb..e854cfa1 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/Directory.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/Directory.kt @@ -1,6 +1,7 @@ package com.kevinschildhorn.fotopresenter.data import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails +import com.kevinschildhorn.fotopresenter.ui.SortingType import com.kevinschildhorn.fotopresenter.ui.shared.SharedImage interface Directory { @@ -38,6 +39,13 @@ data class DirectoryContents( val allDirectories: List get() = folders + images + fun sorted(sortingType: SortingType): DirectoryContents { + return DirectoryContents( + folders = folders.sorted(sortingType) as List, + images = images.sorted(sortingType) as List, + ) + } + override fun toString(): String { return """ DirectoryContents: @@ -48,3 +56,11 @@ data class DirectoryContents( """ } } + +fun List.sorted(sortingType: SortingType): List = + when (sortingType) { + SortingType.NAME_ASC -> this.sortedBy { it.name } + SortingType.NAME_DESC -> this.sortedByDescending { it.name } + SortingType.TIME_ASC -> this.sortedBy { it.details.dateMillis } + SortingType.TIME_DESC -> this.sortedByDescending { it.details.dateMillis } + } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSource.kt index 8efca7d5..bb9ecfdd 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSource.kt @@ -1,5 +1,6 @@ package com.kevinschildhorn.fotopresenter.data.datasources +import co.touchlab.kermit.Logger import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler import com.kevinschildhorn.fotopresenter.data.network.NetworkHandlerError @@ -9,11 +10,21 @@ import kotlin.coroutines.cancellation.CancellationException /** Fetches Directory info from a NAS **/ -class DirectoryDataSource(private val networkHandler: NetworkHandler) { +class DirectoryDataSource( + private val networkHandler: NetworkHandler, + private val logger: Logger, +) { @Throws(NetworkHandlerException::class, CancellationException::class) suspend fun changeDirectory(directoryName: String): String { + logger.i { "Changing directory to $directoryName" } + logger.i { "Is network Connected? ${networkHandler.isConnected}" } if (!networkHandler.isConnected) throw NetworkHandlerException(NetworkHandlerError.NOT_CONNECTED) + logger.i { "Does the directory exist?" } + val exists = networkHandler.folderExists(directoryName) + logger.i { "Does the directory exist? $exists" } + + logger.i { "Opening the directory..." } networkHandler.openDirectory(directoryName)?.let { return it } throw NetworkHandlerException(NetworkHandlerError.DIRECTORY_NOT_FOUND) } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/NetworkDirectoryDetails.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/NetworkDirectoryDetails.kt index a2d40c47..40cb6568 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/NetworkDirectoryDetails.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/NetworkDirectoryDetails.kt @@ -1,9 +1,11 @@ package com.kevinschildhorn.fotopresenter.data.network import com.kevinschildhorn.fotopresenter.data.supportedImageTypes +import kotlinx.datetime.Clock interface NetworkDirectoryDetails { val fullPath: String + val dateMillis:Long val id: Int val fileName: String @@ -24,4 +26,5 @@ interface NetworkDirectoryDetails { class DefaultNetworkDirectoryDetails( override val fullPath: String, override val id: Int, + override val dateMillis: Long = Clock.System.now().toEpochMilliseconds() ) : NetworkDirectoryDetails diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/NetworkHandler.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/NetworkHandler.kt index eaaf2095..060d17ba 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/NetworkHandler.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/NetworkHandler.kt @@ -17,6 +17,8 @@ interface NetworkHandler { suspend fun openDirectory(path: String): String? suspend fun openImage(path: String): SharedImage? + + suspend fun folderExists(path: String): Boolean? } class NetworkHandlerException : Exception { diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SortingType.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SortingType.kt new file mode 100644 index 00000000..ea0fe2dc --- /dev/null +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SortingType.kt @@ -0,0 +1,8 @@ +package com.kevinschildhorn.fotopresenter.ui + +enum class SortingType { + NAME_ASC, + NAME_DESC, + TIME_ASC, + TIME_DESC, +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ImageViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ImageViewModel.kt index 76436936..9afcf440 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ImageViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ImageViewModel.kt @@ -9,6 +9,7 @@ import com.kevinschildhorn.fotopresenter.extension.getNextIndex import com.kevinschildhorn.fotopresenter.extension.getPreviousIndex import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -34,12 +35,14 @@ interface ImageViewModel { } fun clearPresentedImage() + fun cancelImageJobs() } class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel, KoinComponent { private val _uiState = MutableStateFlow(ImageScreenState()) override var scope: CoroutineScope? = null override val imageUiState: StateFlow = _uiState.asStateFlow() + private val jobs: MutableList = mutableListOf() override fun setImageDirectories(directories: List) { _uiState.update { it.copy(imageDirectories = directories) } @@ -79,6 +82,13 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel _uiState.update { it.copy(selectedImage = null, selectedImageIndex = null) } } + override fun cancelImageJobs() { + jobs.forEach { + it.cancel() + } + jobs.clear() + } + private fun updateSelectedImage() { logger?.i { "Updating Selected Index" } with(imageUiState.value) { @@ -102,6 +112,8 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel _uiState.update { it.copy(selectedImage = imageBitmap) } } } + }?.let { + jobs.add(it) } } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/composables/FilterDialog.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/composables/FilterDialog.kt new file mode 100644 index 00000000..e34c337c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/composables/FilterDialog.kt @@ -0,0 +1,56 @@ +package com.kevinschildhorn.fotopresenter.ui.screens.common.composables + +import androidx.compose.foundation.layout.Row +import androidx.compose.material.RadioButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import com.kevinschildhorn.fotopresenter.ui.SortingType +import com.kevinschildhorn.fotopresenter.ui.screens.common.CommonAtoms.dialogButton + +@Composable +fun FilterDialog( + dialogTitle: String, + onDismissRequest: () -> Unit, + onConfirmation: (SortingType) -> Unit, +) { + val selectedOption = remember { mutableStateOf(SortingType.NAME_ASC) } + + FotoDialog( + dialogTitle, + onDismissRequest = onDismissRequest, + onConfirmation = { + onConfirmation(selectedOption.value) + }, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = selectedOption.value == SortingType.NAME_ASC, + onClick = { selectedOption.value = SortingType.NAME_ASC } + ) + AtomikText("File Name A-Z",dialogButton) + } + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = selectedOption.value == SortingType.NAME_DESC, + onClick = { selectedOption.value = SortingType.NAME_DESC } + ) + AtomikText("File Name Z-A",dialogButton) + } + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = selectedOption.value == SortingType.TIME_ASC, + onClick = { selectedOption.value = SortingType.TIME_ASC } + ) + AtomikText("Time Created Ascending",dialogButton) + } + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = selectedOption.value == SortingType.TIME_DESC, + onClick = { selectedOption.value = SortingType.TIME_DESC } + ) + AtomikText("Time Created Descending",dialogButton) + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryAtoms.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryAtoms.kt index a5a02b79..772d5c60 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryAtoms.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryAtoms.kt @@ -19,6 +19,11 @@ import compose.icons.evaicons.fill.Folder object DirectoryAtoms { val emptyDirectory = EmptyPhotoMolecule() + val ImageTicker = SimpleTextAtom( + textColor = FotoColors.backgroundText, + typography = FotoTypography.caption, + fontFamily = null, + ) val navigationItem = TextButtonMolecule( color = FotoColors.secondary, diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreen.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreen.kt index 0c618faa..7aba9dcd 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreen.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreen.kt @@ -1,38 +1,41 @@ package com.kevinschildhorn.fotopresenter.ui.screens.directory +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.TextButton +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.style.TextAlign +import com.kevinschildhorn.atomik.atomic.atoms.textStyle import com.kevinschildhorn.atomik.color.base.composeColor import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails import com.kevinschildhorn.fotopresenter.ui.UiState -import com.kevinschildhorn.fotopresenter.ui.atoms.FotoColors import com.kevinschildhorn.fotopresenter.ui.atoms.Padding import com.kevinschildhorn.fotopresenter.ui.screens.common.ActionSheetAction import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.ActionSheet import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.ConfirmationDialog import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.ErrorView +import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.FilterDialog import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.ImagePreviewOverlay import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.LoadingOverlay +import com.kevinschildhorn.fotopresenter.ui.screens.directory.DirectoryAtoms.ImageTicker import com.kevinschildhorn.fotopresenter.ui.screens.directory.composables.grid.DirectoryGrid import com.kevinschildhorn.fotopresenter.ui.screens.directory.composables.navbar.DirectoryNavigationBar -import com.kevinschildhorn.fotopresenter.ui.screens.directory.composables.navrail.NavigationRailMenuButton +import com.kevinschildhorn.fotopresenter.ui.screens.directory.composables.navrail.DirectoryTitleBarButton import com.kevinschildhorn.fotopresenter.ui.screens.directory.composables.navrail.NavigationRailOverlay import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistScreen import compose.icons.EvaIcons import compose.icons.evaicons.Fill +import compose.icons.evaicons.fill.Funnel import compose.icons.evaicons.fill.Menu enum class DirectoryOverlay { @@ -41,6 +44,7 @@ enum class DirectoryOverlay { NAV_RAIL, LOGOUT_CONFIRMATION, PLAYLIST, + FILTER, NONE, } @@ -66,9 +70,15 @@ fun DirectoryScreen( //region UI Column { - NavigationRailMenuButton { - overlayVisible = DirectoryOverlay.NAV_RAIL + Row(horizontalArrangement = Arrangement.SpaceBetween) { + DirectoryTitleBarButton(EvaIcons.Fill.Menu) { + overlayVisible = DirectoryOverlay.NAV_RAIL + } + DirectoryTitleBarButton(EvaIcons.Fill.Funnel) { + overlayVisible = DirectoryOverlay.FILTER + } } + (uiState.state as? UiState.ERROR)?.let { ErrorView( it.message, @@ -88,6 +98,13 @@ fun DirectoryScreen( }, modifier = Modifier.padding(Padding.SMALL.dp) ) + Text( + uiState.imageCountString, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = ImageTicker.textStyle, + color = ImageTicker.textColor.composeColor + ) DirectoryGrid( uiState.directoryGridState, onFolderPressed = { @@ -193,6 +210,18 @@ fun DirectoryScreen( } //endregion + if (overlayVisible == DirectoryOverlay.FILTER) { + FilterDialog( + "Filter Images by", + onDismissRequest = { + overlayVisible = DirectoryOverlay.NONE + }, + onConfirmation = { + viewModel.setFilterType(it) + } + ) + } + //region Playlist if (overlayVisible == DirectoryOverlay.PLAYLIST) { PlaylistScreen( diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreenState.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreenState.kt index f0319457..aec4fc76 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreenState.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreenState.kt @@ -3,6 +3,7 @@ package com.kevinschildhorn.fotopresenter.ui.screens.directory import androidx.compose.ui.graphics.ImageBitmap import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails import com.kevinschildhorn.fotopresenter.data.State +import com.kevinschildhorn.fotopresenter.ui.SortingType import com.kevinschildhorn.fotopresenter.ui.UiState import com.kevinschildhorn.fotopresenter.ui.screens.common.ActionSheetAction import com.kevinschildhorn.fotopresenter.ui.screens.common.ActionSheetContext @@ -14,6 +15,9 @@ data class DirectoryScreenState( val slideshowDetails: ImageSlideshowDetails? = null, val loggedIn: Boolean = true, val selectedDirectory: DirectoryGridCellState? = null, + val currentImageCount: Int = 0, + val totalImageCount: Int = 0, + val sortingType: SortingType = SortingType.NAME_ASC, override val state: UiState = UiState.IDLE, ) : ScreenState { fun copyImageState( @@ -42,6 +46,13 @@ data class DirectoryScreenState( val currentPathList: List get() = currentPath.split("\\").filter { it.isNotEmpty() } + + val imageCountString: String + get() = + if (totalImageCount != 0 && currentImageCount != totalImageCount) + "$currentImageCount of $totalImageCount downloaded" + else "" + } data class DirectoryGridState( diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt index 0c50b6dd..6ae64c05 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt @@ -1,14 +1,11 @@ package com.kevinschildhorn.fotopresenter.ui.screens.directory import co.touchlab.kermit.Logger -import com.kevinschildhorn.fotopresenter.Playlist import com.kevinschildhorn.fotopresenter.data.Directory import com.kevinschildhorn.fotopresenter.data.DirectoryContents -import com.kevinschildhorn.fotopresenter.data.ImageDirectory import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails import com.kevinschildhorn.fotopresenter.data.PlaylistDetails import com.kevinschildhorn.fotopresenter.data.State -import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails import com.kevinschildhorn.fotopresenter.data.network.NetworkHandlerException import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase @@ -18,12 +15,12 @@ import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageDirectoriesUs import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageUseCase import com.kevinschildhorn.fotopresenter.extension.addPath import com.kevinschildhorn.fotopresenter.extension.navigateBackToPathAtIndex +import com.kevinschildhorn.fotopresenter.ui.SortingType import com.kevinschildhorn.fotopresenter.ui.UiState import com.kevinschildhorn.fotopresenter.ui.screens.common.ActionSheetContext import com.kevinschildhorn.fotopresenter.ui.screens.common.DefaultImageViewModel import com.kevinschildhorn.fotopresenter.ui.screens.common.ImageViewModel import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel -import com.kevinschildhorn.fotopresenter.ui.shared.ViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -46,7 +43,8 @@ class DirectoryViewModel( private val _directoryContentsState = MutableStateFlow(DirectoryContents()) - private val jobs: List = emptyList() + private val downloadedImageSet: MutableSet = mutableSetOf() + private val jobs: MutableList = mutableListOf() private val currentPath: String get() = uiState.value.currentPath @@ -69,6 +67,7 @@ class DirectoryViewModel( } fun logout() { + cancelJobs() viewModelScope.launch(Dispatchers.Default) { logger.i { "Logging Out" } val logoutUseCase: DisconnectFromServerUseCase by inject() @@ -84,13 +83,15 @@ class DirectoryViewModel( fun startSlideshow() { logger.i { "Starting Slideshow" } + cancelJobs() uiState.value.selectedDirectory?.id?.let { id -> _directoryContentsState.value.folders.find { it.id == id }?.let { - viewModelScope.launch(Dispatchers.Default) { + val job = viewModelScope.launch(Dispatchers.Default) { val retrieveImagesUseCase: RetrieveImageDirectoriesUseCase by inject() val images = retrieveImagesUseCase(it.details) _uiState.update { it.copy(slideshowDetails = ImageSlideshowDetails(images)) } } + jobs.add(job) } } ?: run { logger.w { "No Directory Selected!" } @@ -134,6 +135,7 @@ class DirectoryViewModel( private fun changeDirectoryToPath(path: String) { logger.i { "Changing directory to path $path" } + cancelJobs() viewModelScope.launch(Dispatchers.Default) { val changeDirectoryUseCase: ChangeDirectoryUseCase by inject() try { @@ -169,7 +171,7 @@ class DirectoryViewModel( private fun updateDirectories() { logger.i { "Updating Directories" } _uiState.update { it.copy(state = UiState.LOADING) } - viewModelScope.launch(Dispatchers.Default) { + val job = viewModelScope.launch(Dispatchers.Default) { val retrieveDirectoryUseCase: RetrieveDirectoryContentsUseCase by inject() logger.i { "Getting Directory Contents" } @@ -177,25 +179,22 @@ class DirectoryViewModel( logger.i { "Got Directory Contents: ${directoryContents.allDirectories.count()}" } _directoryContentsState.update { directoryContents } - logger.i { "Updating State to Success" } - logger.i { "Setting Directories: $directoryContents" } - setImageDirectories(directoryContents.images) - val gridState = directoryContents.asDirectoryGridState - logger.i { "New Grid State $gridState" } - _uiState.update { - it.copy( - directoryGridState = gridState, - state = UiState.SUCCESS, - ) - } + updateGrid() logger.i { "Current State ${uiState.value.state}" } updatePhotos() } + jobs.add(job) } private fun updatePhotos() { - imageUiState.value.imageDirectories.forEach { imageDirectory -> - viewModelScope.launch(Dispatchers.Default) { + val count = imageUiState.value.imageDirectories.count() + downloadedImageSet.clear() + + _uiState.update { it.copy(totalImageCount = count, currentImageCount = 0) } + + logger.i { "Updating Photos" } + imageUiState.value.imageDirectories.forEachIndexed { index, imageDirectory -> + val job = viewModelScope.launch(Dispatchers.Default) { val retrieveImagesUseCase: RetrieveImageUseCase by inject() retrieveImagesUseCase(imageDirectory) { newState -> @@ -204,9 +203,28 @@ class DirectoryViewModel( imageDirectory.id, state = newState, ) + downloadedImageSet.add(index) + it.copy( + currentImageCount = downloadedImageSet.size + ) } } } + jobs.add(job) + } + } + + private fun updateGrid() = with(_directoryContentsState.value) { + logger.i { "Updating State to Success" } + logger.i { "Setting Directories: $this" } + setImageDirectories(this.images) + val gridState = this.asDirectoryGridState + logger.i { "New Grid State $gridState" } + _uiState.update { + it.copy( + directoryGridState = gridState, + state = UiState.SUCCESS, + ) } } @@ -250,4 +268,19 @@ class DirectoryViewModel( } //endregion + fun setFilterType(sortingType: SortingType) { + _directoryContentsState.update { + it.sorted(sortingType) + } + updateGrid() + } + + private fun cancelJobs() { + logger.i { "Cancelling Jobs!" } + cancelImageJobs() + jobs.forEach { + it.cancel() + } + jobs.clear() + } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/navrail/NavigationRailMenuButton.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/navrail/DirectoryTitleBarButton.kt similarity index 74% rename from shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/navrail/NavigationRailMenuButton.kt rename to shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/navrail/DirectoryTitleBarButton.kt index 5ea968f7..52fa5eef 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/navrail/NavigationRailMenuButton.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/navrail/DirectoryTitleBarButton.kt @@ -6,15 +6,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import com.kevinschildhorn.atomik.color.base.composeColor import com.kevinschildhorn.fotopresenter.ui.atoms.FotoColors -import com.kevinschildhorn.fotopresenter.ui.screens.directory.DirectoryOverlay -import compose.icons.EvaIcons -import compose.icons.evaicons.Fill -import compose.icons.evaicons.fill.Menu import androidx.compose.ui.Modifier import androidx.compose.foundation.layout.size +import androidx.compose.ui.graphics.vector.ImageVector @Composable -fun NavigationRailMenuButton( +fun DirectoryTitleBarButton( + imageVector: ImageVector, onClick: () -> Unit ) { TextButton( @@ -22,7 +20,7 @@ fun NavigationRailMenuButton( onClick = onClick ) { Icon( - EvaIcons.Fill.Menu, + imageVector, contentDescription = "Menu", tint = FotoColors.backgroundText.composeColor ) diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/login/LoginViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/login/LoginViewModel.kt index 27bc5b75..aefeff57 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/login/LoginViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/login/LoginViewModel.kt @@ -27,10 +27,10 @@ class LoginViewModel( val credentials = credentialsRepository.fetchCredentials() _uiState.update { it.copy( - hostname = credentials.hostname, - username = credentials.username, - password = credentials.password, - sharedFolder = credentials.sharedFolder, + hostname = "192.168.1.190",//credentials.hostname, + username = "kevin",//credentials.username, + password = "9E^54qFq^z",//credentials.password, + sharedFolder = "Photos",//credentials.sharedFolder, shouldAutoConnect = credentials.shouldAutoConnect, ) } diff --git a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJHandler.kt b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJHandler.kt index 8f57f9c9..0bf63591 100644 --- a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJHandler.kt +++ b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJHandler.kt @@ -82,19 +82,27 @@ object SMBJHandler : NetworkHandler { } override suspend fun openImage(path: String): SharedImage? { - share?.openFile( - path, - accessMask, - attributes, - shareAccesses, - createDisposition, - createOptions, - )?.let { - return SharedImage(it) + try { + share?.openFile( + path, + accessMask, + attributes, + shareAccesses, + createDisposition, + createOptions, + )?.let { + return SharedImage(it) + } + } catch (e: Exception) { + return null } return null } + override suspend fun folderExists(path: String): Boolean? { + return share?.folderExists(path) + } + override suspend fun disconnect() { share?.close() session?.close() diff --git a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJNetworkDirectoryDetails.kt b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJNetworkDirectoryDetails.kt index a5a50bed..b93a74be 100644 --- a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJNetworkDirectoryDetails.kt +++ b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJNetworkDirectoryDetails.kt @@ -7,4 +7,7 @@ class SMBJNetworkDirectoryDetails( override val fullPath: String, ) : NetworkDirectoryDetails { override val id: Int = information.fileId.toInt() + + override val dateMillis: Long = information.changeTime.toEpochMillis() + } From 8bee09eac021727e7f5bdfd9ea3ff727f52d4fbe Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 22 Dec 2023 21:21:21 -0500 Subject: [PATCH 04/11] updating desktop --- .gitignore | 2 ++ desktopApp/build.gradle.kts | 2 ++ .../data/datasources/DirectoryDataSource.kt | 4 +-- .../resources/compose-multiplatform.xml | 36 ------------------- shared/src/desktopMain/kotlin/Main.desktop.kt | 20 ++++++----- 5 files changed, 18 insertions(+), 46 deletions(-) delete mode 100644 shared/src/commonMain/resources/compose-multiplatform.xml diff --git a/.gitignore b/.gitignore index 41471ba5..8cef73e5 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ ios/FotoPresenter.xcworkspace/xcuserdata/kevinschildhorn.xcuserdatad/UserInterfa ios/FotoPresenter.xcworkspace/xcuserdata/kevinschildhorn.xcuserdatad/UserInterfaceState.xcuserstate common/src/commonMain/kotlin/me/kevinschildhorn/common/network/ftps/TestingLoginInfo.kt androidApp/build/kotlin/compileDebugKotlinAndroid/cacheable/caches-jvm/lookups/lookups.tab_i.len + +desktopApp/build diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 89f3a578..238be32c 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -11,6 +11,8 @@ kotlin { val jvmMain by getting { dependencies { implementation(compose.desktop.currentOs) + implementation("io.insert-koin:koin-core:3.4.0") + implementation(project(":shared")) } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSource.kt index bb9ecfdd..86f6b6e7 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSource.kt @@ -21,8 +21,8 @@ class DirectoryDataSource( if (!networkHandler.isConnected) throw NetworkHandlerException(NetworkHandlerError.NOT_CONNECTED) logger.i { "Does the directory exist?" } - val exists = networkHandler.folderExists(directoryName) - logger.i { "Does the directory exist? $exists" } + //val exists = networkHandler.folderExists(directoryName) + //logger.i { "Does the directory exist? $exists" } logger.i { "Opening the directory..." } networkHandler.openDirectory(directoryName)?.let { return it } diff --git a/shared/src/commonMain/resources/compose-multiplatform.xml b/shared/src/commonMain/resources/compose-multiplatform.xml deleted file mode 100644 index d7bf7955..00000000 --- a/shared/src/commonMain/resources/compose-multiplatform.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - diff --git a/shared/src/desktopMain/kotlin/Main.desktop.kt b/shared/src/desktopMain/kotlin/Main.desktop.kt index 1ac27e86..868146ed 100644 --- a/shared/src/desktopMain/kotlin/Main.desktop.kt +++ b/shared/src/desktopMain/kotlin/Main.desktop.kt @@ -1,11 +1,15 @@ -actual fun getPlatformName(): String = "Desktop" -/* -private val viewModel: LoginViewModel by inject() +import androidx.compose.runtime.Composable +import com.kevinschildhorn.fotopresenter.ui.screens.directory.DirectoryViewModel +import com.kevinschildhorn.fotopresenter.ui.screens.login.LoginViewModel +import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel +import com.kevinschildhorn.fotopresenter.ui.screens.slideshow.SlideshowViewModel -@Composable fun MainView() = App(viewModel) +actual fun getPlatformName(): String = "Desktop" -@Preview @Composable -fun AppPreview() { - App(viewModel) -}*/ +fun MainView( + loginViewModel: LoginViewModel, + directoryViewModel: DirectoryViewModel, + slideshowViewModel: SlideshowViewModel, + playlistViewModel: PlaylistViewModel, +) = App(loginViewModel, directoryViewModel, slideshowViewModel, playlistViewModel) From 1c816fb01f99c13428da2d11a68be6dfd69bd843 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Sat, 23 Dec 2023 20:20:19 -0500 Subject: [PATCH 05/11] Fixing connection --- .../domain/image/RetrieveImageUseCase.kt | 14 ++++++--- .../ui/screens/directory/DirectoryScreen.kt | 31 ++++++++++++++----- .../screens/directory/DirectoryScreenState.kt | 2 +- .../screens/directory/DirectoryViewModel.kt | 8 +++-- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt index f697a759..176503b2 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt @@ -23,37 +23,43 @@ class RetrieveImageUseCase( directory: ImageDirectory, callback: suspend (State) -> Unit, ) { - logger.i { "Starting to get Image ${directory.name}" } + val imageName = "\"${directory.details.fullPath}\"" + logger.i { "Starting to get Image $imageName" } callback(State.LOADING) var workingWidth: Int = 0 var updatedImage: Boolean = false + imageCacheDataSource.getImage(directory.details)?.let { - logger.i { "Image found in cache, using that" } + logger.i { "$imageName found in cache, using that" } callback(State.SUCCESS(it)) workingWidth = it.width } - - logger.i { "Getting Image Bitmap from File ${directory.name}" } + logger.i { "Getting Image Bitmap from File $imageName" } if (workingWidth <= IMAGE_SIZE_SMALL) { + logger.i { "Getting Small Bitmap for $imageName" } + val smallImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_SMALL) callback(smallImageBitmap.asState) workingWidth = IMAGE_SIZE_SMALL updatedImage = true } if (workingWidth <= IMAGE_SIZE_MEDIUM) { + logger.i { "Getting Medium Image" } val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_MEDIUM) callback(mediumImageBitmap.asState) workingWidth = IMAGE_SIZE_MEDIUM updatedImage = true } if (workingWidth <= IMAGE_SIZE_LARGE) { + logger.i { "Getting Large Image" } val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_LARGE) callback(mediumImageBitmap.asState) workingWidth = IMAGE_SIZE_LARGE updatedImage = true } if (workingWidth <= IMAGE_SIZE_EXTRA_LARGE || !updatedImage) { + logger.i { "Getting Extra Large Image" } val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_EXTRA_LARGE) callback(mediumImageBitmap.asState) workingWidth = IMAGE_SIZE_EXTRA_LARGE diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreen.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreen.kt index 7aba9dcd..9dcde4a3 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreen.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreen.kt @@ -4,7 +4,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column 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.shape.RoundedCornerShape +import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -14,11 +17,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import com.kevinschildhorn.atomik.atomic.atoms.textStyle import com.kevinschildhorn.atomik.color.base.composeColor import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails import com.kevinschildhorn.fotopresenter.ui.UiState +import com.kevinschildhorn.fotopresenter.ui.atoms.FotoColors import com.kevinschildhorn.fotopresenter.ui.atoms.Padding import com.kevinschildhorn.fotopresenter.ui.screens.common.ActionSheetAction import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.ActionSheet @@ -98,13 +104,24 @@ fun DirectoryScreen( }, modifier = Modifier.padding(Padding.SMALL.dp) ) - Text( - uiState.imageCountString, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = ImageTicker.textStyle, - color = ImageTicker.textColor.composeColor - ) + if (uiState.imageCountString.isNotEmpty()) { + Text( + uiState.imageCountString, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = ImageTicker.textStyle, + color = ImageTicker.textColor.composeColor + ) + LinearProgressIndicator( + progress = uiState.currentImageCount.toFloat() / uiState.totalImageCount.toFloat(), + modifier = Modifier.fillMaxWidth() + .height(25.dp) + .padding(horizontal = Padding.EXTRA_LARGE.dp, vertical = Padding.SMALL.dp) + .clip(RoundedCornerShape(5.dp)), + color = FotoColors.primary.composeColor, + ) + + } DirectoryGrid( uiState.directoryGridState, onFolderPressed = { diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreenState.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreenState.kt index aec4fc76..d920adae 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreenState.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryScreenState.kt @@ -49,7 +49,7 @@ data class DirectoryScreenState( val imageCountString: String get() = - if (totalImageCount != 0 && currentImageCount != totalImageCount) + if (totalImageCount != 0 && currentImageCount < totalImageCount) "$currentImageCount of $totalImageCount downloaded" else "" diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt index 6ae64c05..1dbbe592 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt @@ -122,6 +122,7 @@ class DirectoryViewModel( fun navigateToFolder(folderIndex: Int) { logger.i { "Getting path at index $folderIndex" } + cancelJobs() val finalPath = currentPath.navigateBackToPathAtIndex(folderIndex) changeDirectoryToPath(finalPath) } @@ -198,13 +199,13 @@ class DirectoryViewModel( val retrieveImagesUseCase: RetrieveImageUseCase by inject() retrieveImagesUseCase(imageDirectory) { newState -> + + downloadedImageSet.add(index) _uiState.update { it.copyImageState( imageDirectory.id, state = newState, - ) - downloadedImageSet.add(index) - it.copy( + ).copy( currentImageCount = downloadedImageSet.size ) } @@ -269,6 +270,7 @@ class DirectoryViewModel( //endregion fun setFilterType(sortingType: SortingType) { + logger.i { "Setting Filter Type" } _directoryContentsState.update { it.sorted(sortingType) } From 6ea779b209f2037435736f4be0140c20936f3988 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Sat, 23 Dec 2023 21:34:12 -0500 Subject: [PATCH 06/11] Fixing Buttons --- .../ui/screens/slideshow/SlideshowScreen.kt | 127 ++++++++++++++---- .../screens/slideshow/SlideshowViewModel.kt | 1 - .../fotopresenter/data/network/SMBJHandler.kt | 9 +- 3 files changed, 112 insertions(+), 25 deletions(-) diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowScreen.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowScreen.kt index e698b205..3fafd7bc 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowScreen.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowScreen.kt @@ -1,32 +1,46 @@ package com.kevinschildhorn.fotopresenter.ui.screens.slideshow +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.material.Button +import androidx.compose.foundation.layout.size import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect 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.graphics.Color import androidx.compose.ui.unit.dp import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.LoadingOverlay import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.Overlay -import com.kevinschildhorn.fotopresenter.ui.screens.common.composables.PrimaryIconButton import compose.icons.EvaIcons import compose.icons.evaicons.Fill import compose.icons.evaicons.fill.ArrowLeft import compose.icons.evaicons.fill.ArrowRight import compose.icons.evaicons.fill.Close +import kotlinx.coroutines.delay +@OptIn(ExperimentalFoundationApi::class) @Composable fun SlideshowScreen( viewModel: SlideshowViewModel, @@ -35,13 +49,24 @@ fun SlideshowScreen( val uiState by viewModel.uiState.collectAsState() val imageState by viewModel.imageUiState.collectAsState() + var show by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + while (true) { + delay(5000L) + show = false + } + } + Box(modifier = Modifier.fillMaxSize().background(Color.Black)) { imageState.selectedImage?.let { bitmap -> - Image( - bitmap = bitmap, - contentDescription = null, - modifier = Modifier.fillMaxSize(), - ) + Crossfade(imageState.selectedImageIndex, animationSpec = tween(500)){ + Image( + bitmap = bitmap, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + ) + } } ?: kotlin.run { LoadingOverlay() } @@ -56,34 +81,90 @@ fun SlideshowScreen( ) { Column( modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.SpaceBetween ) { Row( modifier = Modifier.fillMaxWidth().height(44.dp), horizontalArrangement = Arrangement.End, ) { - PrimaryIconButton( - EvaIcons.Fill.Close, - onClick = { + AnimatedVisibility( + visible = show, + enter = fadeIn(animationSpec = tween(500)), + exit = fadeOut(animationSpec = tween(500)) + ) { + TextButton(onClick = { viewModel.stopSlideshow() onDismiss() + }) { + Icon( + EvaIcons.Fill.Close, + tint = Color.White, + contentDescription = "Close", + modifier = Modifier.size(55.dp) + ) } - ) + } } Row( - modifier = Modifier.fillMaxWidth().height(44.dp), + modifier = Modifier.fillMaxWidth().fillMaxHeight(), horizontalArrangement = Arrangement.SpaceBetween, ) { - - PrimaryIconButton(EvaIcons.Fill.ArrowLeft, - onClick = { - viewModel.skipBackwards() - }) - PrimaryIconButton(EvaIcons.Fill.ArrowRight, - onClick = { - viewModel.skipForward() - }) + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(0.5f) + .combinedClickable( + onClick = { + show = true + }, + onDoubleClick = { + viewModel.skipBackwards() + }, + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxSize() + ) { + if (show) { + Icon( + EvaIcons.Fill.ArrowLeft, + tint = Color.White, + contentDescription = "Left", + modifier = Modifier.size(55.dp) + ) + } + } + } + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .combinedClickable( + onClick = { + show = true + }, + onDoubleClick = { + viewModel.skipForward() + }, + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxSize() + ) { + if (show) { + Icon( + EvaIcons.Fill.ArrowRight, + tint = Color.White, + contentDescription = "Right", + modifier = Modifier.size(55.dp) + ) + } + } + } } } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowViewModel.kt index e9bbd827..0bdd2a0e 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowViewModel.kt @@ -31,7 +31,6 @@ class SlideshowViewModel( private val _uiState = MutableStateFlow(SlideshowScreenState()) val uiState: StateFlow = _uiState.asStateFlow() private var timer: Timer? = null - var firstImageTimer: Job? = null init { setImageScope(viewModelScope) diff --git a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJHandler.kt b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJHandler.kt index 0bf63591..5e1309e4 100644 --- a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJHandler.kt +++ b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/data/network/SMBJHandler.kt @@ -20,7 +20,14 @@ object SMBJHandler : NetworkHandler { private var session: Session? = null private var share: DiskShare? = null - private val accessMask: Set = setOf(AccessMask.FILE_READ_DATA) + private val accessMask: Set = + setOf( + AccessMask.FILE_READ_DATA, + AccessMask.FILE_LIST_DIRECTORY, + AccessMask.FILE_TRAVERSE, + AccessMask.FILE_READ_ATTRIBUTES, + AccessMask.GENERIC_READ, + ) private val attributes: Set = setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL) private val shareAccesses: Set = setOf(SMB2ShareAccess.FILE_SHARE_READ) private val createDisposition: SMB2CreateDisposition = SMB2CreateDisposition.FILE_OPEN From 5015624aed8fde8bde163f6396db544ac10eccdd Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Sun, 31 Dec 2023 15:21:29 -0500 Subject: [PATCH 07/11] Updating settings --- desktopApp/build.gradle.kts | 2 ++ desktopApp/src/jvmMain/kotlin/Main.kt | 24 ++++++++++++++++++++++-- gradle.properties | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 238be32c..37d582e4 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -12,6 +12,8 @@ kotlin { dependencies { implementation(compose.desktop.currentOs) implementation("io.insert-koin:koin-core:3.4.0") + implementation("co.touchlab:kermit:1.2.2") + implementation("com.russhwolf:multiplatform-settings:1.0.0") implementation(project(":shared")) } diff --git a/desktopApp/src/jvmMain/kotlin/Main.kt b/desktopApp/src/jvmMain/kotlin/Main.kt index c814b116..7b2b4cea 100644 --- a/desktopApp/src/jvmMain/kotlin/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/Main.kt @@ -1,9 +1,29 @@ -import androidx.compose.material.Text import androidx.compose.ui.window.Window import androidx.compose.ui.window.application +import com.kevinschildhorn.fotopresenter.UseCaseFactory +import com.kevinschildhorn.fotopresenter.ui.screens.directory.DirectoryViewModel +import com.kevinschildhorn.fotopresenter.ui.screens.login.LoginViewModel +import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel +import com.kevinschildhorn.fotopresenter.ui.screens.slideshow.SlideshowViewModel + + +object KoinPurse { + val loginViewModel = + LoginViewModel(UseCaseFactory.baseLogger, UseCaseFactory.credentialsRepository) + val directoryViewModel = + DirectoryViewModel(UseCaseFactory.playlistRepository, UseCaseFactory.baseLogger) + val slideshowViewModel = SlideshowViewModel(UseCaseFactory.baseLogger) + val playlistViewModel = + PlaylistViewModel(UseCaseFactory.playlistRepository, UseCaseFactory.baseLogger) +} fun main() = application { Window(onCloseRequest = ::exitApplication) { - Text("") + MainView( + KoinPurse.loginViewModel, + KoinPurse.directoryViewModel, + KoinPurse.slideshowViewModel, + KoinPurse.playlistViewModel, + ) } } diff --git a/gradle.properties b/gradle.properties index bd4c947f..04040306 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,4 @@ android.minSdk=22 #Versions kotlin.version=1.9.21 agp.version=8.1.4 -compose.version=1.5.11 \ No newline at end of file +compose.version=1.5.11 From b4aefcf01159dd00fa4be05465bcc03c5ad91a9a Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Sun, 31 Dec 2023 15:21:37 -0500 Subject: [PATCH 08/11] Adding factory --- .../fotopresenter/UseCaseFactoryAndroid.kt | 56 +++++++++++ .../com/kevinschildhorn/fotopresenter/Koin.kt | 3 +- .../fotopresenter/UseCaseFactory.kt | 21 +++++ .../RetrieveSlideshowFromPlaylistUseCase.kt | 3 +- .../fotopresenter/UseCaseFactoryDesktop.kt | 94 +++++++++++++++++++ 5 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt create mode 100644 shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt create mode 100644 shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt new file mode 100644 index 00000000..f05d218d --- /dev/null +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt @@ -0,0 +1,56 @@ +package com.kevinschildhorn.fotopresenter + +import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.AutoConnectUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.ConnectToServerUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.SaveCredentialsUseCase +import com.kevinschildhorn.fotopresenter.domain.directory.ChangeDirectoryUseCase +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageDirectoriesUseCase +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageUseCase +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveSlideshowFromPlaylistUseCase +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +actual object UseCaseFactory : KoinComponent { + + val connectToServerUseCase: ConnectToServerUseCase + get() { + val useCase: ConnectToServerUseCase by inject() + return useCase + } + val changeDirectoryUseCase: ChangeDirectoryUseCase + get() { + val useCase: ChangeDirectoryUseCase by inject() + return useCase + } + val autoConnectUseCase: AutoConnectUseCase + get() { + val useCase: AutoConnectUseCase by inject() + return useCase + } + val saveCredentialsUseCase: SaveCredentialsUseCase + get() { + val useCase: SaveCredentialsUseCase by inject() + return useCase + } + val retrieveImageDirectoriesUseCase: RetrieveImageDirectoriesUseCase + get() { + val useCase: RetrieveImageDirectoriesUseCase by inject() + return useCase + } + val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase + get() { + val useCase: RetrieveSlideshowFromPlaylistUseCase by inject() + return useCase + } + val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase + get() { + val useCase: RetrieveDirectoryContentsUseCase by inject() + return useCase + } + val retrieveImageUseCase: RetrieveImageUseCase + get() { + val useCase: RetrieveImageUseCase by inject() + return useCase + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt index f3a9f259..f6bffdd6 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt @@ -29,9 +29,10 @@ import com.kevinschildhorn.fotopresenter.ui.shared.SharedCache import org.koin.core.module.Module import org.koin.dsl.module +val baseLogger = Logger(LoggerConfig.default) + val commonModule = module { - val baseLogger = Logger(LoggerConfig.default) // Data single { SharedCache } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt new file mode 100644 index 00000000..9947687b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt @@ -0,0 +1,21 @@ +package com.kevinschildhorn.fotopresenter + +import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.AutoConnectUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.ConnectToServerUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.SaveCredentialsUseCase +import com.kevinschildhorn.fotopresenter.domain.directory.ChangeDirectoryUseCase +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageDirectoriesUseCase +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageUseCase +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveSlideshowFromPlaylistUseCase + +expect object UseCaseFactory { + val connectToServerUseCase: ConnectToServerUseCase + val changeDirectoryUseCase: ChangeDirectoryUseCase + val autoConnectUseCase: AutoConnectUseCase + val saveCredentialsUseCase: SaveCredentialsUseCase + val retrieveImageDirectoriesUseCase: RetrieveImageDirectoriesUseCase + val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase + val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase + val retrieveImageUseCase: RetrieveImageUseCase +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveSlideshowFromPlaylistUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveSlideshowFromPlaylistUseCase.kt index 6f2c713e..586cf76b 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveSlideshowFromPlaylistUseCase.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveSlideshowFromPlaylistUseCase.kt @@ -14,13 +14,12 @@ Retrieving Slideshow Details From Playlist Details **/ class RetrieveSlideshowFromPlaylistUseCase( private val logger: Logger, + private val retrieveDirectoryUseCase: RetrieveImageDirectoriesUseCase, ) : KoinComponent { suspend operator fun invoke( playlistDetails: PlaylistDetails, ): ImageSlideshowDetails { logger.i { "Starting to get details from playlist ${playlistDetails.name}" } - val retrieveDirectoryUseCase: RetrieveImageDirectoriesUseCase by inject() - val directories: List = playlistDetails.items.map { item -> val directoryDetails = DefaultNetworkDirectoryDetails( id = item.directory_id.toInt(), diff --git a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt new file mode 100644 index 00000000..2f4eec46 --- /dev/null +++ b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt @@ -0,0 +1,94 @@ +package com.kevinschildhorn.fotopresenter + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.LoggerConfig +import com.kevinschildhorn.fotopresenter.data.datasources.CredentialsDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.DirectoryDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.ImageCacheDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.ImageRemoteDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistDataSource +import com.kevinschildhorn.fotopresenter.data.network.SMBJHandler +import com.kevinschildhorn.fotopresenter.data.repositories.CredentialsRepository +import com.kevinschildhorn.fotopresenter.data.repositories.DirectoryRepository +import com.kevinschildhorn.fotopresenter.data.repositories.ImageRepository +import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository +import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.AutoConnectUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.ConnectToServerUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.SaveCredentialsUseCase +import com.kevinschildhorn.fotopresenter.domain.directory.ChangeDirectoryUseCase +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageDirectoriesUseCase +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageUseCase +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveSlideshowFromPlaylistUseCase +import com.kevinschildhorn.fotopresenter.ui.shared.DriverFactory +import com.kevinschildhorn.fotopresenter.ui.shared.SharedCache +import com.russhwolf.settings.PreferencesSettings +import java.util.prefs.Preferences + +actual object UseCaseFactory { + + val baseLogger = Logger(LoggerConfig.default) + private val preferences: Preferences = Preferences.userRoot() + private val settings = PreferencesSettings(preferences) + + private val directoryDataSource = DirectoryDataSource( + SMBJHandler, + baseLogger.withTag("DirectoryDataSource") + ) + private val credentialDataSource = CredentialsDataSource(settings) + val credentialsRepository = CredentialsRepository(credentialDataSource) + private val directoryRepository = DirectoryRepository(directoryDataSource) + private val imageRepository = ImageRepository(ImageRemoteDataSource(SMBJHandler)) + private val playlistDataSource = PlaylistDataSource(DriverFactory().createDriver(), com.kevinschildhorn.fotopresenter.baseLogger) + val playlistRepository = PlaylistRepository(playlistDataSource) + + + val connectToServerUseCase: ConnectToServerUseCase + get() = ConnectToServerUseCase( + client = SMBJHandler, + logger = baseLogger.withTag("ConnectToServerUseCase") + ) + val changeDirectoryUseCase: ChangeDirectoryUseCase + get() = ChangeDirectoryUseCase( + dataSource = DirectoryDataSource( + SMBJHandler, + baseLogger.withTag("DirectoryDataSource") + ), + logger = baseLogger.withTag("ChangeDirectoryUseCase") + ) + val autoConnectUseCase: AutoConnectUseCase + get() = AutoConnectUseCase( + client = SMBJHandler, + repository = credentialsRepository, + logger = baseLogger.withTag("AutoConnectUseCase") + ) + val saveCredentialsUseCase: SaveCredentialsUseCase + get() = SaveCredentialsUseCase( + repository = credentialsRepository, + logger = baseLogger.withTag("SaveCredentialsUseCase") + ) + val retrieveImageDirectoriesUseCase: RetrieveImageDirectoriesUseCase + get() = RetrieveImageDirectoriesUseCase( + logger = baseLogger.withTag("RetrieveImageDirectoriesUseCase") + ) + val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase + get() = RetrieveSlideshowFromPlaylistUseCase( + logger = baseLogger.withTag("RetrieveSlideshowFromPlaylistUseCase"), + retrieveDirectoryUseCase = this.retrieveImageDirectoriesUseCase, + ) + val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase + get() = RetrieveDirectoryContentsUseCase( + directoryRepository = directoryRepository, + imageRepository = imageRepository, + logger = baseLogger.withTag("RetrieveDirectoryContentsUseCase") + ) + val retrieveImageUseCase: RetrieveImageUseCase + get() = RetrieveImageUseCase( + imageCacheDataSource = ImageCacheDataSource( + cache = SharedCache, + driver = DriverFactory().createDriver(), + logger = baseLogger.withTag("ImageCacheDataSource") + ), + logger = baseLogger.withTag("RetrieveImageUseCase") + ) +} \ No newline at end of file From 314ec884cdf02729bc3b5ce9b845e9b2e609b866 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Sun, 31 Dec 2023 16:30:04 -0500 Subject: [PATCH 09/11] moving use cases to UseCaseFactory --- .../fotopresenter/UseCaseFactoryAndroid.kt | 24 +++++++---- .../com/kevinschildhorn/fotopresenter/Koin.kt | 7 +++- .../fotopresenter/UseCaseFactory.kt | 2 + .../image/RetrieveImageDirectoriesUseCase.kt | 8 ++-- .../ui/screens/common/ImageViewModel.kt | 5 +-- .../screens/directory/DirectoryViewModel.kt | 17 +++----- .../ui/screens/login/LoginViewModel.kt | 11 ++--- .../screens/slideshow/SlideshowViewModel.kt | 8 +--- .../fotopresenter/UseCaseFactoryDesktop.kt | 42 ++++++++++++------- 9 files changed, 67 insertions(+), 57 deletions(-) diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt index f05d218d..4f694b2a 100644 --- a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt @@ -3,6 +3,7 @@ package com.kevinschildhorn.fotopresenter import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase import com.kevinschildhorn.fotopresenter.domain.connection.AutoConnectUseCase import com.kevinschildhorn.fotopresenter.domain.connection.ConnectToServerUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.DisconnectFromServerUseCase import com.kevinschildhorn.fotopresenter.domain.connection.SaveCredentialsUseCase import com.kevinschildhorn.fotopresenter.domain.directory.ChangeDirectoryUseCase import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageDirectoriesUseCase @@ -12,43 +13,48 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject actual object UseCaseFactory : KoinComponent { - - val connectToServerUseCase: ConnectToServerUseCase + + actual val connectToServerUseCase: ConnectToServerUseCase get() { val useCase: ConnectToServerUseCase by inject() return useCase } - val changeDirectoryUseCase: ChangeDirectoryUseCase + actual val changeDirectoryUseCase: ChangeDirectoryUseCase get() { val useCase: ChangeDirectoryUseCase by inject() return useCase } - val autoConnectUseCase: AutoConnectUseCase + actual val autoConnectUseCase: AutoConnectUseCase get() { val useCase: AutoConnectUseCase by inject() return useCase } - val saveCredentialsUseCase: SaveCredentialsUseCase + actual val saveCredentialsUseCase: SaveCredentialsUseCase get() { val useCase: SaveCredentialsUseCase by inject() return useCase } - val retrieveImageDirectoriesUseCase: RetrieveImageDirectoriesUseCase + actual val disconnectFromServerUseCase: DisconnectFromServerUseCase + get() { + val useCase: DisconnectFromServerUseCase by inject() + return useCase + } + actual val retrieveImageDirectoriesUseCase: RetrieveImageDirectoriesUseCase get() { val useCase: RetrieveImageDirectoriesUseCase by inject() return useCase } - val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase + actual val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase get() { val useCase: RetrieveSlideshowFromPlaylistUseCase by inject() return useCase } - val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase + actual val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase get() { val useCase: RetrieveDirectoryContentsUseCase by inject() return useCase } - val retrieveImageUseCase: RetrieveImageUseCase + actual val retrieveImageUseCase: RetrieveImageUseCase get() { val useCase: RetrieveImageUseCase by inject() return useCase diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt index f6bffdd6..b267cc4d 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt @@ -59,7 +59,12 @@ val commonModule = ) } factory { RetrieveImageDirectoriesUseCase(baseLogger.withTag("RetrieveImageDirectoriesUseCase")) } - factory { RetrieveSlideshowFromPlaylistUseCase(baseLogger.withTag("RetrieveSlideshowFromPlaylistUseCase")) } + factory { + RetrieveSlideshowFromPlaylistUseCase( + baseLogger.withTag("RetrieveSlideshowFromPlaylistUseCase"), + get() + ) + } factory { RetrieveDirectoryContentsUseCase( get(), diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt index 9947687b..632ea05b 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt @@ -3,6 +3,7 @@ package com.kevinschildhorn.fotopresenter import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase import com.kevinschildhorn.fotopresenter.domain.connection.AutoConnectUseCase import com.kevinschildhorn.fotopresenter.domain.connection.ConnectToServerUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.DisconnectFromServerUseCase import com.kevinschildhorn.fotopresenter.domain.connection.SaveCredentialsUseCase import com.kevinschildhorn.fotopresenter.domain.directory.ChangeDirectoryUseCase import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageDirectoriesUseCase @@ -14,6 +15,7 @@ expect object UseCaseFactory { val changeDirectoryUseCase: ChangeDirectoryUseCase val autoConnectUseCase: AutoConnectUseCase val saveCredentialsUseCase: SaveCredentialsUseCase + val disconnectFromServerUseCase: DisconnectFromServerUseCase val retrieveImageDirectoriesUseCase: RetrieveImageDirectoriesUseCase val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageDirectoriesUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageDirectoriesUseCase.kt index 149738f5..c8de06a8 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageDirectoriesUseCase.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageDirectoriesUseCase.kt @@ -1,11 +1,10 @@ package com.kevinschildhorn.fotopresenter.domain.image import co.touchlab.kermit.Logger +import com.kevinschildhorn.fotopresenter.UseCaseFactory import com.kevinschildhorn.fotopresenter.data.ImageDirectory import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails -import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase import org.koin.core.component.KoinComponent -import org.koin.core.component.inject /** Retrieving Image Directories from Path @@ -18,7 +17,7 @@ class RetrieveImageDirectoriesUseCase( recursively: Boolean = true, ): List { logger.i { "Retrieving images from directory ${directoryDetails.fullPath}" } - val retrieveContentsUseCase: RetrieveDirectoryContentsUseCase by inject() + val retrieveContentsUseCase = UseCaseFactory.retrieveDirectoryContentsUseCase val contents = retrieveContentsUseCase(directoryDetails.fullPath) logger.i { "Retrieved Contents: $contents" } @@ -30,9 +29,8 @@ class RetrieveImageDirectoriesUseCase( if (recursively) { logger.i { "Recursively getting Images from sub-folder" } - val recursiveUseCase: RetrieveImageDirectoriesUseCase by inject() folders.forEach { - images.addAll(recursiveUseCase(it.details)) + images.addAll(invoke(it.details)) } } return images diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ImageViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ImageViewModel.kt index 9afcf440..24408181 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ImageViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ImageViewModel.kt @@ -4,7 +4,6 @@ import androidx.compose.ui.graphics.ImageBitmap import co.touchlab.kermit.Logger import com.kevinschildhorn.fotopresenter.data.ImageDirectory import com.kevinschildhorn.fotopresenter.data.State -import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageUseCase import com.kevinschildhorn.fotopresenter.extension.getNextIndex import com.kevinschildhorn.fotopresenter.extension.getPreviousIndex import kotlinx.coroutines.CoroutineScope @@ -16,7 +15,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent -import org.koin.core.component.inject +import com.kevinschildhorn.fotopresenter.UseCaseFactory interface ImageViewModel { var scope: CoroutineScope? @@ -104,7 +103,7 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel private fun showPhoto(imageDirectory: ImageDirectory) { logger?.i { "Showing Photo" } scope?.launch(Dispatchers.Default) { - val retrieveImagesUseCase: RetrieveImageUseCase by inject() + val retrieveImagesUseCase = UseCaseFactory.retrieveImageUseCase logger?.d { "Retrieving Image" } retrieveImagesUseCase(imageDirectory) { newState: State -> logger?.d { "Image State Updated $newState" } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt index 1dbbe592..dc4d642a 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt @@ -6,13 +6,9 @@ import com.kevinschildhorn.fotopresenter.data.DirectoryContents import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails import com.kevinschildhorn.fotopresenter.data.PlaylistDetails import com.kevinschildhorn.fotopresenter.data.State +import com.kevinschildhorn.fotopresenter.UseCaseFactory import com.kevinschildhorn.fotopresenter.data.network.NetworkHandlerException import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository -import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase -import com.kevinschildhorn.fotopresenter.domain.connection.DisconnectFromServerUseCase -import com.kevinschildhorn.fotopresenter.domain.directory.ChangeDirectoryUseCase -import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageDirectoriesUseCase -import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageUseCase import com.kevinschildhorn.fotopresenter.extension.addPath import com.kevinschildhorn.fotopresenter.extension.navigateBackToPathAtIndex import com.kevinschildhorn.fotopresenter.ui.SortingType @@ -29,7 +25,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent -import org.koin.core.component.inject class DirectoryViewModel( private val playlistRepository: PlaylistRepository, @@ -70,7 +65,7 @@ class DirectoryViewModel( cancelJobs() viewModelScope.launch(Dispatchers.Default) { logger.i { "Logging Out" } - val logoutUseCase: DisconnectFromServerUseCase by inject() + val logoutUseCase = UseCaseFactory.disconnectFromServerUseCase logoutUseCase() logger.d { "Setting loggedIn state to false" } _uiState.update { it.copy(loggedIn = false) } @@ -87,7 +82,7 @@ class DirectoryViewModel( uiState.value.selectedDirectory?.id?.let { id -> _directoryContentsState.value.folders.find { it.id == id }?.let { val job = viewModelScope.launch(Dispatchers.Default) { - val retrieveImagesUseCase: RetrieveImageDirectoriesUseCase by inject() + val retrieveImagesUseCase = UseCaseFactory.retrieveImageDirectoriesUseCase val images = retrieveImagesUseCase(it.details) _uiState.update { it.copy(slideshowDetails = ImageSlideshowDetails(images)) } } @@ -138,7 +133,7 @@ class DirectoryViewModel( cancelJobs() viewModelScope.launch(Dispatchers.Default) { - val changeDirectoryUseCase: ChangeDirectoryUseCase by inject() + val changeDirectoryUseCase = UseCaseFactory.changeDirectoryUseCase try { logger.i { "Getting New Path" } val newPath = changeDirectoryUseCase(path) @@ -173,7 +168,7 @@ class DirectoryViewModel( logger.i { "Updating Directories" } _uiState.update { it.copy(state = UiState.LOADING) } val job = viewModelScope.launch(Dispatchers.Default) { - val retrieveDirectoryUseCase: RetrieveDirectoryContentsUseCase by inject() + val retrieveDirectoryUseCase = UseCaseFactory.retrieveDirectoryContentsUseCase logger.i { "Getting Directory Contents" } val directoryContents = retrieveDirectoryUseCase(currentPath) @@ -196,7 +191,7 @@ class DirectoryViewModel( logger.i { "Updating Photos" } imageUiState.value.imageDirectories.forEachIndexed { index, imageDirectory -> val job = viewModelScope.launch(Dispatchers.Default) { - val retrieveImagesUseCase: RetrieveImageUseCase by inject() + val retrieveImagesUseCase = UseCaseFactory.retrieveImageUseCase retrieveImagesUseCase(imageDirectory) { newState -> diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/login/LoginViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/login/LoginViewModel.kt index aefeff57..f102ac72 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/login/LoginViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/login/LoginViewModel.kt @@ -1,10 +1,8 @@ package com.kevinschildhorn.fotopresenter.ui.screens.login import co.touchlab.kermit.Logger +import com.kevinschildhorn.fotopresenter.UseCaseFactory import com.kevinschildhorn.fotopresenter.data.repositories.CredentialsRepository -import com.kevinschildhorn.fotopresenter.domain.connection.AutoConnectUseCase -import com.kevinschildhorn.fotopresenter.domain.connection.ConnectToServerUseCase -import com.kevinschildhorn.fotopresenter.domain.connection.SaveCredentialsUseCase import com.kevinschildhorn.fotopresenter.ui.UiState import com.kevinschildhorn.fotopresenter.ui.shared.ViewModel import kotlinx.coroutines.Dispatchers @@ -14,7 +12,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent -import org.koin.core.component.inject class LoginViewModel( private val logger: Logger, @@ -64,7 +61,7 @@ class LoginViewModel( _uiState.update { it.copy(state = UiState.LOADING) } - val connectToServer: ConnectToServerUseCase by inject() + val connectToServer = UseCaseFactory.connectToServerUseCase viewModelScope.launch(Dispatchers.Default) { logger.i { "Connecting To Server With Credentials" } @@ -82,7 +79,7 @@ class LoginViewModel( _uiState.update { it.copy(state = UiState.SUCCESS) } logger.i { "Saving Credentials" } logger.i { "State is ${uiState.value}" } - val saveCredentials: SaveCredentialsUseCase by inject() + val saveCredentials = UseCaseFactory.saveCredentialsUseCase saveCredentials(_uiState.value.asLoginCredentials) } } @@ -95,7 +92,7 @@ class LoginViewModel( private fun attemptAutoLogin() { logger.i { "Attempting To Auto Login" } viewModelScope.launch(Dispatchers.Default) { - val autoConnectUseCase: AutoConnectUseCase by inject() + val autoConnectUseCase = UseCaseFactory.autoConnectUseCase if (autoConnectUseCase()) { _uiState.update { it.copy(state = UiState.SUCCESS) } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowViewModel.kt index 0bdd2a0e..db690920 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/slideshow/SlideshowViewModel.kt @@ -3,13 +3,10 @@ package com.kevinschildhorn.fotopresenter.ui.screens.slideshow import co.touchlab.kermit.Logger import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails import com.kevinschildhorn.fotopresenter.data.PlaylistDetails -import com.kevinschildhorn.fotopresenter.domain.image.RetrieveSlideshowFromPlaylistUseCase import com.kevinschildhorn.fotopresenter.ui.screens.common.DefaultImageViewModel import com.kevinschildhorn.fotopresenter.ui.screens.common.ImageViewModel import com.kevinschildhorn.fotopresenter.ui.shared.ViewModel -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -17,10 +14,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import java.util.* import kotlin.concurrent.fixedRateTimer -import kotlin.coroutines.EmptyCoroutineContext +import com.kevinschildhorn.fotopresenter.UseCaseFactory class SlideshowViewModel( private val logger: Logger, @@ -38,7 +34,7 @@ class SlideshowViewModel( fun setSlideshowFromPlaylist(playlistDetails: PlaylistDetails) { logger.i { "Starting playlist $playlistDetails" } - val useCase: RetrieveSlideshowFromPlaylistUseCase by inject() + val useCase = UseCaseFactory.retrieveSlideshowFromPlaylistUseCase viewModelScope.launch(Dispatchers.Default) { val slideshow = useCase(playlistDetails) setSlideshow(slideshow) diff --git a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt index 2f4eec46..ffc53520 100644 --- a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt +++ b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt @@ -7,6 +7,7 @@ import com.kevinschildhorn.fotopresenter.data.datasources.DirectoryDataSource import com.kevinschildhorn.fotopresenter.data.datasources.ImageCacheDataSource import com.kevinschildhorn.fotopresenter.data.datasources.ImageRemoteDataSource import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistDataSource +import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler import com.kevinschildhorn.fotopresenter.data.network.SMBJHandler import com.kevinschildhorn.fotopresenter.data.repositories.CredentialsRepository import com.kevinschildhorn.fotopresenter.data.repositories.DirectoryRepository @@ -15,6 +16,7 @@ import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase import com.kevinschildhorn.fotopresenter.domain.connection.AutoConnectUseCase import com.kevinschildhorn.fotopresenter.domain.connection.ConnectToServerUseCase +import com.kevinschildhorn.fotopresenter.domain.connection.DisconnectFromServerUseCase import com.kevinschildhorn.fotopresenter.domain.connection.SaveCredentialsUseCase import com.kevinschildhorn.fotopresenter.domain.directory.ChangeDirectoryUseCase import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageDirectoriesUseCase @@ -23,6 +25,7 @@ import com.kevinschildhorn.fotopresenter.domain.image.RetrieveSlideshowFromPlayl import com.kevinschildhorn.fotopresenter.ui.shared.DriverFactory import com.kevinschildhorn.fotopresenter.ui.shared.SharedCache import com.russhwolf.settings.PreferencesSettings +import org.koin.core.component.inject import java.util.prefs.Preferences actual object UseCaseFactory { @@ -30,59 +33,68 @@ actual object UseCaseFactory { val baseLogger = Logger(LoggerConfig.default) private val preferences: Preferences = Preferences.userRoot() private val settings = PreferencesSettings(preferences) - + private val networkHandler: NetworkHandler = SMBJHandler private val directoryDataSource = DirectoryDataSource( - SMBJHandler, + networkHandler, baseLogger.withTag("DirectoryDataSource") ) private val credentialDataSource = CredentialsDataSource(settings) val credentialsRepository = CredentialsRepository(credentialDataSource) private val directoryRepository = DirectoryRepository(directoryDataSource) - private val imageRepository = ImageRepository(ImageRemoteDataSource(SMBJHandler)) - private val playlistDataSource = PlaylistDataSource(DriverFactory().createDriver(), com.kevinschildhorn.fotopresenter.baseLogger) + private val imageRepository = ImageRepository(ImageRemoteDataSource(networkHandler)) + private val playlistDataSource = PlaylistDataSource( + DriverFactory().createDriver(), + com.kevinschildhorn.fotopresenter.baseLogger + ) val playlistRepository = PlaylistRepository(playlistDataSource) - val connectToServerUseCase: ConnectToServerUseCase + actual val connectToServerUseCase: ConnectToServerUseCase get() = ConnectToServerUseCase( - client = SMBJHandler, + client = networkHandler, logger = baseLogger.withTag("ConnectToServerUseCase") ) - val changeDirectoryUseCase: ChangeDirectoryUseCase + actual val changeDirectoryUseCase: ChangeDirectoryUseCase get() = ChangeDirectoryUseCase( dataSource = DirectoryDataSource( - SMBJHandler, + networkHandler, baseLogger.withTag("DirectoryDataSource") ), logger = baseLogger.withTag("ChangeDirectoryUseCase") ) - val autoConnectUseCase: AutoConnectUseCase + actual val autoConnectUseCase: AutoConnectUseCase get() = AutoConnectUseCase( - client = SMBJHandler, + client = networkHandler, repository = credentialsRepository, logger = baseLogger.withTag("AutoConnectUseCase") ) - val saveCredentialsUseCase: SaveCredentialsUseCase + actual val saveCredentialsUseCase: SaveCredentialsUseCase get() = SaveCredentialsUseCase( repository = credentialsRepository, logger = baseLogger.withTag("SaveCredentialsUseCase") ) - val retrieveImageDirectoriesUseCase: RetrieveImageDirectoriesUseCase + actual val disconnectFromServerUseCase: DisconnectFromServerUseCase + get() = DisconnectFromServerUseCase( + credentialsRepository, + networkHandler, + baseLogger.withTag("DisconnectFromServerUseCase") + ) + actual val retrieveImageDirectoriesUseCase: RetrieveImageDirectoriesUseCase get() = RetrieveImageDirectoriesUseCase( logger = baseLogger.withTag("RetrieveImageDirectoriesUseCase") ) - val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase + actual val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase get() = RetrieveSlideshowFromPlaylistUseCase( logger = baseLogger.withTag("RetrieveSlideshowFromPlaylistUseCase"), retrieveDirectoryUseCase = this.retrieveImageDirectoriesUseCase, ) - val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase + actual val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase get() = RetrieveDirectoryContentsUseCase( directoryRepository = directoryRepository, imageRepository = imageRepository, logger = baseLogger.withTag("RetrieveDirectoryContentsUseCase") ) - val retrieveImageUseCase: RetrieveImageUseCase + actual val retrieveImageUseCase: RetrieveImageUseCase get() = RetrieveImageUseCase( imageCacheDataSource = ImageCacheDataSource( cache = SharedCache, From c36629e95076810a20cc1eeb34607155a62ab6a5 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Sun, 31 Dec 2023 16:37:23 -0500 Subject: [PATCH 10/11] Adding right click --- .../directory/composables/grid/DirectoryGrid.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/grid/DirectoryGrid.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/grid/DirectoryGrid.kt index b04468ec..366b8702 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/grid/DirectoryGrid.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/composables/grid/DirectoryGrid.kt @@ -9,6 +9,9 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex @@ -37,6 +40,16 @@ fun DirectoryGrid( val directoryItemModifier = Modifier .padding(5.dp) + .pointerInput(Unit) { + awaitPointerEventScope { + val event = awaitPointerEvent() + if (event.type == PointerEventType.Press && + event.buttons.isSecondaryPressed) { + event.changes.forEach { e -> e.consume() } + onActionSheet(state) + } + } + } .combinedClickable( onClick = { (state as? ImageDirectoryGridCellState)?.let { imageContent -> From 972eea29357205a4e482a4755da0252813e75c14 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Sun, 31 Dec 2023 16:40:26 -0500 Subject: [PATCH 11/11] Update Main.kt --- desktopApp/src/jvmMain/kotlin/Main.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desktopApp/src/jvmMain/kotlin/Main.kt b/desktopApp/src/jvmMain/kotlin/Main.kt index 7b2b4cea..61aff3c3 100644 --- a/desktopApp/src/jvmMain/kotlin/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/Main.kt @@ -18,7 +18,10 @@ object KoinPurse { } fun main() = application { - Window(onCloseRequest = ::exitApplication) { + Window( + title = "FotoPresenter", + onCloseRequest = ::exitApplication + ) { MainView( KoinPurse.loginViewModel, KoinPurse.directoryViewModel,