From bf67ca6b921e523a7c6310de0121a20954f59d56 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Tue, 2 Jan 2024 10:29:24 -0500 Subject: [PATCH 1/5] Adding metadata Data Source --- shared/build.gradle.kts | 1 + .../fotopresenter/data/MetadataDetails.kt | 14 +++++ .../datasources/ImageMetadataDataSource.kt | 57 +++++++++++++++++ .../fotopresenter/ui/shared/SharedImage.kt | 1 + .../ImageMetadataDataSourceTest.kt | 63 +++++++++++++++++++ .../fotopresenter/ui/shared/SharedImage.kt | 1 + .../fotopresenter/ui/shared/SharedImage.kt | 1 + 7 files changed, 138 insertions(+) create mode 100644 shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/MetadataDetails.kt create mode 100644 shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt create mode 100644 shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSourceTest.kt diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index f00f542d..b27f9b45 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -44,6 +44,7 @@ kotlin { 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") + implementation("com.ashampoo:kim:0.8.3") 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/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/MetadataDetails.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/MetadataDetails.kt new file mode 100644 index 00000000..f638dd91 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/MetadataDetails.kt @@ -0,0 +1,14 @@ +package com.kevinschildhorn.fotopresenter.data + +import kotlinx.serialization.Serializable + +@Serializable +data class MetadataDetails( + val files: List +) + +@Serializable +data class MetadataFileDetails( + val filePath: String, + val tags: List, +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt new file mode 100644 index 00000000..c44302bc --- /dev/null +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt @@ -0,0 +1,57 @@ +package com.kevinschildhorn.fotopresenter.data.datasources + +import co.touchlab.kermit.Logger +import com.ashampoo.kim.Kim +import com.ashampoo.kim.common.convertToPhotoMetadata +import com.ashampoo.kim.format.tiff.constants.ExifTag +import com.kevinschildhorn.fotopresenter.data.MetadataDetails +import com.kevinschildhorn.fotopresenter.data.MetadataFileDetails +import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class ImageMetadataDataSource( + private val logger: Logger?, + private val networkHandler: NetworkHandler, +) { + + suspend fun importMetaData(): MetadataDetails { + networkHandler.getMetadata()?.let { + return Json.decodeFromString(it) + } + return MetadataDetails(emptyList()) + } + + suspend fun exportMetadata(metadata: MetadataDetails): Boolean { + try { + val jsonString = Json.encodeToString(metadata) + networkHandler.setMetadata(jsonString) + } catch (e: Exception) { + logger?.e(e) { "Error Exporting Metadata" } + return false + } + return true + } + + suspend fun readMetadataFromFile(filePath:String, bytes: ByteArray): MetadataFileDetails? { + networkHandler.openImage(filePath)?.let { sharedImage -> + val metadata = Kim.readMetadata(sharedImage.byteArray) + println(metadata) + + val comments = metadata?.findStringValue(ExifTag.EXIF_TAG_USER_COMMENT) + val keywords = comments?.split(",") ?: emptyList() + + val takenDate = metadata?.findStringValue(ExifTag.EXIF_TAG_DATE_TIME_ORIGINAL) + println("Taken date: $takenDate") + + val photoMetadata = metadata?.convertToPhotoMetadata() + photoMetadata?.orientation + + return MetadataFileDetails( + filePath = filePath, + tags = keywords + ) + } + return null + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt index b5e38795..14dd76ef 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt @@ -4,4 +4,5 @@ import androidx.compose.ui.graphics.ImageBitmap expect class SharedImage { fun getImageBitmap(size: Int): ImageBitmap? + val byteArray: ByteArray } diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSourceTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSourceTest.kt new file mode 100644 index 00000000..5cb0eb58 --- /dev/null +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSourceTest.kt @@ -0,0 +1,63 @@ +package com.kevinschildhorn.fotopresenter.data.datasources + +import com.kevinschildhorn.fotopresenter.data.MetadataFileDetails +import com.kevinschildhorn.fotopresenter.data.network.MockNetworkHandler +import kotlinx.coroutines.runBlocking +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.AfterTest +import kotlin.test.BeforeTest + +/** +Testing [ImageMetadataDataSource] + **/ +class ImageMetadataDataSourceTest { + private val networkHandler: MockNetworkHandler = MockNetworkHandler + + + @BeforeTest + fun startTest() = + runBlocking { + networkHandler.connectSuccessfully() + } + + @AfterTest + fun tearDown() = + runBlocking { + networkHandler.disconnect() + } + + @Test + fun `Import Metadata`() = runBlocking { + val dataSource = ImageMetadataDataSource(null, networkHandler) + val metadata = dataSource.importMetaData() + assertNotNull(metadata) + assertEquals(1, metadata.files.count()) + } + + @Test + fun `Export Playlist`() = runBlocking { + val dataSource = ImageMetadataDataSource(null, networkHandler) + var metadata = dataSource.importMetaData() + assertNotNull(metadata) + assertEquals(0, metadata.files.count()) + + val newMetadataFileDetails = MetadataFileDetails( + "MyPath.png", + listOf("Tag1", "Wallpaper") + ) + val mutableFiles = metadata.files.toMutableList() + mutableFiles.add(newMetadataFileDetails) + metadata = metadata.copy(files = mutableFiles) + + val result = dataSource.exportMetadata(metadata) + assertTrue(result) + + metadata = dataSource.importMetaData() + + assertNotNull(metadata) + assertTrue(metadata.files.contains(newMetadataFileDetails)) + } +} \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt b/shared/src/iosMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt index 0f22bd1e..744138ab 100644 --- a/shared/src/iosMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt +++ b/shared/src/iosMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt @@ -4,4 +4,5 @@ import androidx.compose.ui.graphics.ImageBitmap actual class SharedImage { actual fun getImageBitmap(size:Int): ImageBitmap? = null + actual val byteArray: ByteArray = ByteArray() } diff --git a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt index 8abc7525..2a95e3f1 100644 --- a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt +++ b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt @@ -5,6 +5,7 @@ import com.hierynomus.smbj.share.File actual open class SharedImage(val file: File) { actual fun getImageBitmap(size: Int): ImageBitmap? = getBitmapFromFile(file, size) + actual val byteArray: ByteArray = file.inputStream.readAllBytes() } expect fun getBitmapFromFile(file: File, size: Int): ImageBitmap? From 72e7e0d4a16433ccd9ef249cdb1f39895eccec2e Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 31 Jan 2024 09:16:21 -0500 Subject: [PATCH 2/5] meta --- gradle.properties | 2 +- .../fotopresenter/UseCaseFactoryAndroid.kt | 6 +++ .../ui/compose/CommonPreviews.kt | 2 + .../com/kevinschildhorn/fotopresenter/Koin.kt | 2 +- .../fotopresenter/UseCaseFactory.kt | 2 + .../fotopresenter/data/Directory.kt | 1 + .../fotopresenter/data/MetadataDetails.kt | 9 ++-- .../datasources/ImageMetadataDataSource.kt | 6 +-- .../data/repositories/DirectoryRepository.kt | 21 ++++++-- .../RetrieveDirectoryContentsUseCase.kt | 6 ++- .../RetrieveSlideshowFromPlaylistUseCase.kt | 7 ++- .../image/SaveMetadataForPathUseCase.kt | 32 ++++++++++++ .../ui/screens/common/ActionSheetContext.kt | 1 + .../ui/screens/directory/DirectoryScreen.kt | 18 +++++++ .../screens/directory/DirectoryScreenState.kt | 3 +- .../screens/directory/DirectoryViewModel.kt | 49 ++++++++++++++----- .../ui/screens/playlist/PlaylistScreen.kt | 15 +++--- .../playlist/composables/TextEntryDialog.kt | 6 ++- .../fotopresenter/UseCaseFactoryDesktop.kt | 10 +++- 19 files changed, 163 insertions(+), 35 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/SaveMetadataForPathUseCase.kt diff --git a/gradle.properties b/gradle.properties index 04040306..283cc9cd 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=22 +android.minSdk=23 #Versions kotlin.version=1.9.21 diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt index 4f694b2a..e59af8d6 100644 --- a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryAndroid.kt @@ -9,6 +9,7 @@ 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.domain.image.SaveMetadataForPathUseCase import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -59,4 +60,9 @@ actual object UseCaseFactory : KoinComponent { val useCase: RetrieveImageUseCase by inject() return useCase } + actual val saveMetadataForPathUseCase: SaveMetadataForPathUseCase + get() { + val useCase: SaveMetadataForPathUseCase by inject() + return useCase + } } \ No newline at end of file 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 206c7312..9f7c3569 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 @@ -56,6 +56,8 @@ fun ConfirmationDialogPreview() { @Composable fun TextConfirmationDialogPreview() { TextEntryDialog( + title = "", + initialValue = "", { }, diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt index 7ea15b53..323534c1 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt @@ -42,7 +42,7 @@ val commonModule = single { CredentialsDataSource(get()) } single { CredentialsRepository(get()) } single { DirectoryDataSource(get(), baseLogger.withTag("DirectoryDataSource")) } - single { DirectoryRepository(get()) } + single { DirectoryRepository(get(), get()) } single { ImageRemoteDataSource(get()) } single { ImageRepository(get()) } single { ImageCacheDataSource(get(), get(), baseLogger.withTag("ImageCacheDataSource")) } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt index 632ea05b..d43e875b 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactory.kt @@ -9,6 +9,7 @@ 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.domain.image.SaveMetadataForPathUseCase expect object UseCaseFactory { val connectToServerUseCase: ConnectToServerUseCase @@ -20,4 +21,5 @@ expect object UseCaseFactory { val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase val retrieveImageUseCase: RetrieveImageUseCase + val saveMetadataForPathUseCase: SaveMetadataForPathUseCase } \ No newline at end of file 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 e854cfa1..f663b314 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/Directory.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/Directory.kt @@ -27,6 +27,7 @@ data class FolderDirectory( data class ImageDirectory( override val details: NetworkDirectoryDetails, + val metaData: MetadataFileDetails?, val image: SharedImage? = null, ) : Directory { override fun toString(): String = "(I:${details.fullPath}:${details.id})" diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/MetadataDetails.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/MetadataDetails.kt index f638dd91..e3faf36a 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/MetadataDetails.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/MetadataDetails.kt @@ -4,11 +4,14 @@ import kotlinx.serialization.Serializable @Serializable data class MetadataDetails( - val files: List + val files: MutableList ) @Serializable data class MetadataFileDetails( val filePath: String, - val tags: List, -) \ No newline at end of file + val tags: Set, +) { + val tagsString: String + get() = tags.joinToString(", ") +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt index c44302bc..f966371b 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt @@ -19,7 +19,7 @@ class ImageMetadataDataSource( networkHandler.getMetadata()?.let { return Json.decodeFromString(it) } - return MetadataDetails(emptyList()) + return MetadataDetails(mutableListOf()) } suspend fun exportMetadata(metadata: MetadataDetails): Boolean { @@ -33,7 +33,7 @@ class ImageMetadataDataSource( return true } - suspend fun readMetadataFromFile(filePath:String, bytes: ByteArray): MetadataFileDetails? { + suspend fun readMetadataFromFile(filePath: String): MetadataFileDetails? { networkHandler.openImage(filePath)?.let { sharedImage -> val metadata = Kim.readMetadata(sharedImage.byteArray) println(metadata) @@ -49,7 +49,7 @@ class ImageMetadataDataSource( return MetadataFileDetails( filePath = filePath, - tags = keywords + tags = keywords.toSet() ) } return null diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/DirectoryRepository.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/DirectoryRepository.kt index f84b3440..7bf66afa 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/DirectoryRepository.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/DirectoryRepository.kt @@ -3,19 +3,32 @@ package com.kevinschildhorn.fotopresenter.data.repositories import com.kevinschildhorn.fotopresenter.data.DirectoryContents import com.kevinschildhorn.fotopresenter.data.FolderDirectory import com.kevinschildhorn.fotopresenter.data.ImageDirectory +import com.kevinschildhorn.fotopresenter.data.MetadataFileDetails import com.kevinschildhorn.fotopresenter.data.datasources.DirectoryDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.ImageMetadataDataSource import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails class DirectoryRepository( - private val dataSource: DirectoryDataSource, + private val directoryDataSource: DirectoryDataSource, + private val metadataDataSource: ImageMetadataDataSource, ) { suspend fun getDirectoryContents(path: String): DirectoryContents { - val folderDirectories: List = dataSource.getFolderDirectories(path) - val imageDirectories: List = dataSource.getImageDirectories(path) + val folderDirectories: List = + directoryDataSource.getFolderDirectories(path) + val imageDirectories: List = + directoryDataSource.getImageDirectories(path) + + val metaData = metadataDataSource.importMetaData() + return DirectoryContents( folders = folderDirectories.map { FolderDirectory(it) }, - images = imageDirectories.map { ImageDirectory(it) }, + images = imageDirectories.map { networkDetails -> + ImageDirectory( + networkDetails, + metaData = metaData.files.find { networkDetails.fullPath == it.filePath } + ) + }, ) } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/RetrieveDirectoryContentsUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/RetrieveDirectoryContentsUseCase.kt index 07183856..a50bec8d 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/RetrieveDirectoryContentsUseCase.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/RetrieveDirectoryContentsUseCase.kt @@ -30,6 +30,10 @@ private suspend fun DirectoryContents.updateImages(block: suspend (NetworkDirect this.copy( images = images.map { - ImageDirectory(it.details, image = block(it.details)) + ImageDirectory( + it.details, + it.metaData, + image = block(it.details) + ) }, ) 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 a3d2c77b..b91c57c4 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 @@ -26,7 +26,12 @@ class RetrieveSlideshowFromPlaylistUseCase( fullPath = item.directoryPath ) if (item.directoryPath.isImagePath) { - listOf(ImageDirectory(directoryDetails)) + listOf( + ImageDirectory( + directoryDetails, + null + ) + ) } else { retrieveDirectoryUseCase( directoryDetails = directoryDetails, diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/SaveMetadataForPathUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/SaveMetadataForPathUseCase.kt new file mode 100644 index 00000000..4747db01 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/SaveMetadataForPathUseCase.kt @@ -0,0 +1,32 @@ +package com.kevinschildhorn.fotopresenter.domain.image + +import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails +import com.kevinschildhorn.fotopresenter.data.MetadataFileDetails +import com.kevinschildhorn.fotopresenter.data.datasources.ImageMetadataDataSource +import org.koin.core.component.KoinComponent + +class SaveMetadataForPathUseCase( + private val dataSource: ImageMetadataDataSource, +) : KoinComponent { + + suspend operator fun invoke( + path: String, + tags: String, + ): Boolean { + val tagList: List = tags.split(",").map { it.trim() } + + val metaData = dataSource.importMetaData() + + var fileMetadata: MetadataFileDetails = + metaData.files.find { it.filePath == path } ?: MetadataFileDetails( + filePath = path, + tags = tagList.toSet(), + ) + val newTags = fileMetadata.tags.toMutableSet() + newTags.addAll(tagList.toSet()) + fileMetadata = fileMetadata.copy(tags = newTags) + metaData.files.add(fileMetadata) + + return dataSource.exportMetadata(metaData) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ActionSheetContext.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ActionSheetContext.kt index 9ef8cd27..341c9710 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ActionSheetContext.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/common/ActionSheetContext.kt @@ -4,6 +4,7 @@ enum class ActionSheetAction(val title: String) { START_SLIDESHOW("Start A Slideshow"), ADD_STATIC_LOCATION("Add to a Playlist"), ADD_DYNAMIC_LOCATION("Add dynamically to a Playlist"), + ADD_METADATA("Add metadata to an image"), NONE("Nothing"), // TEMP } 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 9dcde4a3..4917e0ad 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 @@ -39,6 +39,7 @@ import com.kevinschildhorn.fotopresenter.ui.screens.directory.composables.navbar 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 com.kevinschildhorn.fotopresenter.ui.screens.playlist.composables.TextEntryDialog import compose.icons.EvaIcons import compose.icons.evaicons.Fill import compose.icons.evaicons.fill.Funnel @@ -51,6 +52,7 @@ enum class DirectoryOverlay { LOGOUT_CONFIRMATION, PLAYLIST, FILTER, + METADATA, NONE, } @@ -157,6 +159,9 @@ fun DirectoryScreen( ActionSheetAction.ADD_STATIC_LOCATION -> overlayVisible = DirectoryOverlay.PLAYLIST + ActionSheetAction.ADD_METADATA -> + overlayVisible = DirectoryOverlay.METADATA + ActionSheetAction.ADD_DYNAMIC_LOCATION -> overlayVisible = DirectoryOverlay.PLAYLIST @@ -254,4 +259,17 @@ fun DirectoryScreen( } } //endregion + + if (overlayVisible == DirectoryOverlay.METADATA) { + TextEntryDialog( + title = "Add Keywords", + initialValue = viewModel.selectedMetadata?.tagsString ?: "", + { + overlayVisible = DirectoryOverlay.NONE + }, { + viewModel.saveMetadata(it) + viewModel.setSelectedDirectory(null) + overlayVisible = DirectoryOverlay.NONE + }) + } } 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 d920adae..7c4062f1 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 @@ -92,7 +92,8 @@ data class ImageDirectoryGridCellState( ) : DirectoryGridCellState { override val actionSheetContexts: List get() = listOf( - ActionSheetContext(ActionSheetAction.ADD_STATIC_LOCATION, 1) + ActionSheetContext(ActionSheetAction.ADD_STATIC_LOCATION, 1), + ActionSheetContext(ActionSheetAction.ADD_METADATA, 2), ) override fun toString(): String = "(I:$name:$id)" 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 54c0e78f..f012a0e0 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 @@ -7,8 +7,12 @@ 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.FolderDirectory +import com.kevinschildhorn.fotopresenter.data.ImageDirectory +import com.kevinschildhorn.fotopresenter.data.MetadataFileDetails import com.kevinschildhorn.fotopresenter.data.network.NetworkHandlerException import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository +import com.kevinschildhorn.fotopresenter.domain.image.SaveMetadataForPathUseCase import com.kevinschildhorn.fotopresenter.extension.addPath import com.kevinschildhorn.fotopresenter.extension.navigateBackToPathAtIndex import com.kevinschildhorn.fotopresenter.ui.SortingType @@ -47,6 +51,10 @@ class DirectoryViewModel( val actionSheetContexts: List get() = uiState.value.selectedDirectory?.actionSheetContexts ?: emptyList() + + val selectedMetadata: MetadataFileDetails? + get() = findSelectedImageDirectory()?.metaData + init { setImageScope(viewModelScope) } @@ -80,18 +88,15 @@ class DirectoryViewModel( logger.i { "Starting Slideshow" } cancelJobs() logger.d { "Checking for Selected Directory" } - uiState.value.selectedDirectory?.id?.let { id -> - logger.d { "Finding Folder" } - _directoryContentsState.value.folders.find { it.id == id }?.let { - logger.d { "Folder found, starting to retrieve images" } - val job = viewModelScope.launch(Dispatchers.Default) { - val retrieveImagesUseCase = UseCaseFactory.retrieveImageDirectoriesUseCase - val images = retrieveImagesUseCase(it.details) - logger.v { "Retrieved images, copying them to state" } - _uiState.update { it.copy(slideshowDetails = ImageSlideshowDetails(images)) } - } - jobs.add(job) + findSelectedFolderDirectory()?.let { + logger.d { "Folder found, starting to retrieve images" } + val job = viewModelScope.launch(Dispatchers.Default) { + val retrieveImagesUseCase = UseCaseFactory.retrieveImageDirectoriesUseCase + val images = retrieveImagesUseCase(it.details) + logger.v { "Retrieved images, copying them to state" } + _uiState.update { it.copy(slideshowDetails = ImageSlideshowDetails(images)) } } + jobs.add(job) } ?: run { logger.w { "No Directory Selected!" } } @@ -268,6 +273,16 @@ class DirectoryViewModel( } //endregion + fun saveMetadata(metadata: String) { + findSelectedImageDirectory()?.details?.fullPath?.let { + val job = viewModelScope.launch { + val saveMetadataForPathUseCase = UseCaseFactory.saveMetadataForPathUseCase + saveMetadataForPathUseCase(it, metadata) + } + jobs.add(job) + } + } + fun setFilterType(sortingType: SortingType) { logger.i { "Setting Filter Type" } _directoryContentsState.update { @@ -286,4 +301,16 @@ class DirectoryViewModel( logger.v { "Finished Cancelling Jobs!" } } + + private fun findSelectedFolderDirectory(): FolderDirectory? = + uiState.value.selectedDirectory?.id?.let { id -> + logger.d { "Finding Selected Directory" } + return _directoryContentsState.value.folders.find { it.id == id } + } + + private fun findSelectedImageDirectory(): ImageDirectory? = + uiState.value.selectedDirectory?.id?.let { id -> + logger.d { "Finding Selected Directory" } + return _directoryContentsState.value.images.find { it.id == id } + } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/PlaylistScreen.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/PlaylistScreen.kt index 34ed1cc1..a8df055b 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/PlaylistScreen.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/PlaylistScreen.kt @@ -65,12 +65,15 @@ fun PlaylistScreen( when (dialogOpen) { PlaylistDialog.CREATE -> { - TextEntryDialog({ - dialogOpen = PlaylistDialog.NONE - }, { - viewModel.createPlaylist(it) - dialogOpen = PlaylistDialog.NONE - }) + TextEntryDialog( + title = "Create Playlist", + initialValue = "", + { + dialogOpen = PlaylistDialog.NONE + }, { + viewModel.createPlaylist(it) + dialogOpen = PlaylistDialog.NONE + }) } PlaylistDialog.DELETE -> { diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/composables/TextEntryDialog.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/composables/TextEntryDialog.kt index d11dc7b4..4266b0b3 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/composables/TextEntryDialog.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/composables/TextEntryDialog.kt @@ -13,13 +13,15 @@ import com.kevinschildhorn.fotopresenter.ui.screens.login.composables.LoginTextF @Composable fun TextEntryDialog( + title:String, + initialValue: String, onDismissRequest: () -> Unit, onConfirmation: (String) -> Unit, ) { - var enteredValue:String by remember { mutableStateOf("") } + var enteredValue:String by remember { mutableStateOf(initialValue) } FotoDialog( - dialogTitle = "Playlist Name", + dialogTitle = title,//"Playlist Name", onDismissRequest = onDismissRequest, onConfirmation = { onConfirmation(enteredValue) diff --git a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt index 4aec8c21..7ff30a9b 100644 --- a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt +++ b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt @@ -5,6 +5,7 @@ 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.ImageMetadataDataSource import com.kevinschildhorn.fotopresenter.data.datasources.ImageRemoteDataSource import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistFileDataSource import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistSQLDataSource @@ -23,6 +24,7 @@ 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.domain.image.SaveMetadataForPathUseCase import com.kevinschildhorn.fotopresenter.ui.shared.DriverFactory import com.kevinschildhorn.fotopresenter.ui.shared.SharedCache import com.russhwolf.settings.PreferencesSettings @@ -41,7 +43,11 @@ actual object UseCaseFactory { private val sqlDriver = DriverFactory().createDriver() private val credentialDataSource = CredentialsDataSource(settings) val credentialsRepository = CredentialsRepository(credentialDataSource) - private val directoryRepository = DirectoryRepository(directoryDataSource) + private val imageMetadataDataSource = ImageMetadataDataSource( + networkHandler = networkHandler, + logger = baseLogger.withTag("imageMetadataDataSource") + ) + private val directoryRepository = DirectoryRepository(directoryDataSource, imageMetadataDataSource) private val imageRepository = ImageRepository(ImageRemoteDataSource(networkHandler)) private val playlistSQLDataSource = PlaylistSQLDataSource( sqlDriver, @@ -108,4 +114,6 @@ actual object UseCaseFactory { ), logger = baseLogger.withTag("RetrieveImageUseCase") ) + actual val saveMetadataForPathUseCase: SaveMetadataForPathUseCase + get() = SaveMetadataForPathUseCase(dataSource = imageMetadataDataSource) } \ No newline at end of file From 5da764bbcdbed38fba6492d9499652541d61aca7 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 9 Feb 2024 13:48:21 -0500 Subject: [PATCH 3/5] Update DirectoryViewModel.kt --- .../fotopresenter/ui/screens/directory/DirectoryViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 54c0e78f..41acd924 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 @@ -209,8 +209,9 @@ class DirectoryViewModel( ) } } - } - jobs.add(job) + }.awaitAll() + + // TODO: STORE LARGEST IMAGES IN CHUNKS } } From 15ad1c63872656ae0b7e141530e32c534416cf36 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Tue, 13 Feb 2024 16:37:33 -0500 Subject: [PATCH 4/5] Fixing issues with metadata --- .../com/kevinschildhorn/fotopresenter/Koin.kt | 5 ++- .../datasources/ImageMetadataDataSource.kt | 7 ++++ .../image/SaveMetadataForPathUseCase.kt | 17 ++++---- .../screens/directory/DirectoryViewModel.kt | 18 +++----- .../fotopresenter/data/network/SMBJHandler.kt | 41 +++++++++++++++---- 5 files changed, 57 insertions(+), 31 deletions(-) diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt index 323534c1..32f875b1 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt @@ -5,6 +5,7 @@ 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.ImageMetadataDataSource import com.kevinschildhorn.fotopresenter.data.datasources.ImageRemoteDataSource import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistFileDataSource import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistSQLDataSource @@ -21,6 +22,7 @@ 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.domain.image.SaveMetadataForPathUseCase import com.kevinschildhorn.fotopresenter.ui.screens.directory.DirectoryViewModel import com.kevinschildhorn.fotopresenter.ui.screens.login.LoginViewModel import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel @@ -49,6 +51,7 @@ val commonModule = single { PlaylistFileDataSource(baseLogger.withTag("PlaylistDataSource"), get()) } single { PlaylistSQLDataSource(get(), baseLogger.withTag("PlaylistDataSource")) } single { PlaylistRepository(get(), get()) } + factory { ImageMetadataDataSource(baseLogger.withTag("ImageMetadataDataSource"), get()) } // Domain factory { ConnectToServerUseCase(get(), baseLogger.withTag("ConnectToServerUseCase")) } @@ -77,7 +80,7 @@ val commonModule = ) } factory { RetrieveImageUseCase(get(), baseLogger.withTag("RetrieveImagesUseCase")) } - + factory { SaveMetadataForPathUseCase(get()) } // UI single { LoginViewModel(baseLogger.withTag("LoginViewModel"), get()) } single { DirectoryViewModel(get(), baseLogger.withTag("DirectoryViewModel")) } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt index f966371b..739deb85 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSource.kt @@ -16,13 +16,18 @@ class ImageMetadataDataSource( ) { suspend fun importMetaData(): MetadataDetails { + logger?.i { "Importing Metadata" } networkHandler.getMetadata()?.let { + logger?.i { "Found Metadata" } return Json.decodeFromString(it) } + + logger?.i { "No Metadata Found" } return MetadataDetails(mutableListOf()) } suspend fun exportMetadata(metadata: MetadataDetails): Boolean { + logger?.i { "Exporting Metadata" } try { val jsonString = Json.encodeToString(metadata) networkHandler.setMetadata(jsonString) @@ -30,6 +35,8 @@ class ImageMetadataDataSource( logger?.e(e) { "Error Exporting Metadata" } return false } + + logger?.i { "Successfully Exported Metadata" } return true } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/SaveMetadataForPathUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/SaveMetadataForPathUseCase.kt index 4747db01..d078f2c1 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/SaveMetadataForPathUseCase.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/SaveMetadataForPathUseCase.kt @@ -14,18 +14,15 @@ class SaveMetadataForPathUseCase( tags: String, ): Boolean { val tagList: List = tags.split(",").map { it.trim() } - val metaData = dataSource.importMetaData() - var fileMetadata: MetadataFileDetails = - metaData.files.find { it.filePath == path } ?: MetadataFileDetails( - filePath = path, - tags = tagList.toSet(), - ) - val newTags = fileMetadata.tags.toMutableSet() - newTags.addAll(tagList.toSet()) - fileMetadata = fileMetadata.copy(tags = newTags) - metaData.files.add(fileMetadata) + val fileMetadata = MetadataFileDetails( + filePath = path, + tags = tagList.toSet(), + ) + + metaData.files.removeIf { it.filePath == path } + if (fileMetadata.tags.isNotEmpty()) metaData.files.add(fileMetadata) return dataSource.exportMetadata(metaData) } 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 26663301..4be35edf 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 @@ -4,17 +4,12 @@ import co.touchlab.kermit.Logger import com.kevinschildhorn.fotopresenter.UseCaseFactory import com.kevinschildhorn.fotopresenter.data.Directory import com.kevinschildhorn.fotopresenter.data.DirectoryContents +import com.kevinschildhorn.fotopresenter.data.FolderDirectory import com.kevinschildhorn.fotopresenter.data.ImageDirectory import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails +import com.kevinschildhorn.fotopresenter.data.MetadataFileDetails import com.kevinschildhorn.fotopresenter.data.PlaylistDetails import com.kevinschildhorn.fotopresenter.data.State -import com.kevinschildhorn.fotopresenter.UseCaseFactory -import com.kevinschildhorn.fotopresenter.data.FolderDirectory -import com.kevinschildhorn.fotopresenter.data.ImageDirectory -import com.kevinschildhorn.fotopresenter.data.MetadataFileDetails -import com.kevinschildhorn.fotopresenter.data.network.NetworkHandlerException -import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository -import com.kevinschildhorn.fotopresenter.domain.image.SaveMetadataForPathUseCase import com.kevinschildhorn.fotopresenter.data.network.NetworkHandlerException import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageUseCase @@ -31,6 +26,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -112,7 +108,6 @@ class DirectoryViewModel( _uiState.update { it.copy(slideshowDetails = ImageSlideshowDetails(images)) } } } - jobs.add(job) } ?: run { logger.w { "No Directory Selected!" } } @@ -215,7 +210,7 @@ class DirectoryViewModel( logger.i { "Updating Photos" } val retrieveImagesUseCase: RetrieveImageUseCase = UseCaseFactory.retrieveImageUseCase val imageDirectories: List = imageUiState.value.imageDirectories - imageDirectories.mapIndexed{ index, imageDirectory -> + imageDirectories.mapIndexed { index, imageDirectory -> async { retrieveImagesUseCase( imageDirectory, @@ -302,11 +297,10 @@ class DirectoryViewModel( fun saveMetadata(metadata: String) { findSelectedImageDirectory()?.details?.fullPath?.let { - val job = viewModelScope.launch { + viewModelScope.launch(Dispatchers.Default) { val saveMetadataForPathUseCase = UseCaseFactory.saveMetadataForPathUseCase - saveMetadataForPathUseCase(it, metadata) + if (saveMetadataForPathUseCase(it, metadata)) updateDirectories() } - jobs.add(job) } } 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 d920ed73..63033d22 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 @@ -19,6 +19,7 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import java.io.OutputStream import java.util.* +import kotlin.math.log object SMBJHandler : NetworkHandler { @@ -48,6 +49,7 @@ object SMBJHandler : NetworkHandler { override suspend fun connect(credentials: LoginCredentials): Boolean { try { + logger.i { "Connecting" } with(credentials) { connection = client.connect(hostname) val context = AuthenticationContext(username, password.toCharArray(), null) @@ -68,6 +70,7 @@ object SMBJHandler : NetworkHandler { } override suspend fun getDirectoryDetails(path: String): NetworkDirectoryDetails? { + logger.i { "Getting Directory Details for $path" } share?.getFileInformation(path)?.let { return DefaultNetworkDirectoryDetails( id = it.internalInformation.indexNumber.toInt(), @@ -79,6 +82,7 @@ object SMBJHandler : NetworkHandler { } override suspend fun getDirectoryContents(path: String): List { + logger.i { "Getting Directory Contents for $path" } return share?.list(path)?.map { SMBJNetworkDirectoryDetails( it, @@ -88,6 +92,7 @@ object SMBJHandler : NetworkHandler { } override suspend fun openDirectory(path: String): String? { + logger.i { "Opening Directory $path" } val result = share?.openDirectory( path, accessMask, @@ -101,7 +106,9 @@ object SMBJHandler : NetworkHandler { override suspend fun openImage(path: String): SharedImage? = getFile(path)?.let { - SharedImage(it) + val sharedImage = SharedImage(it) + it.close() + sharedImage } ?: run { null } override suspend fun folderExists(path: String): Boolean? { @@ -125,13 +132,22 @@ object SMBJHandler : NetworkHandler { .filter { it.fileExtension == "json" } .filter { !it.fileName.contains(metaDataName) } .mapNotNull { getFile(it.fullPath) } - .map { it.inputStream.readAllBytes().decodeToString() } + .map { + val str = it.inputStream.readAllBytes().decodeToString() + it.close() + str + } - override suspend fun setMetadata(json: String): Boolean = - writeFile(fileName = metaDataName, contents = json) + override suspend fun setMetadata(json: String): Boolean { + logger.i { "Setting Metadata" } + return writeFile(fileName = metaDataName, contents = json) + } override suspend fun getMetadata(): String? = getFile(metaDataName)?.let { - it.inputStream.readAllBytes().decodeToString() + logger.i { "Importing Metadata" } + val str = it.inputStream.readAllBytes().decodeToString() + it.close() + str } override suspend fun deletePlaylist(playlistName: String) { @@ -143,6 +159,7 @@ object SMBJHandler : NetworkHandler { path: String, ): File? = try { + logger.v { "Getting File at path $path" } share?.openFile( path, accessMask, @@ -152,33 +169,41 @@ object SMBJHandler : NetworkHandler { createOptions, ) } catch (e: Exception) { + logger.e(e) { "Error Getting File" } null } - private fun writeFile(fileName: String, contents: String): Boolean { + private suspend fun writeFile(fileName: String, contents: String): Boolean { + logger.i { "Writing File" } val fileAttributes: MutableSet = HashSet() fileAttributes.add(FileAttributes.FILE_ATTRIBUTE_NORMAL) val createOptions: MutableSet = HashSet() createOptions.add(SMB2CreateOptions.FILE_RANDOM_ACCESS) try { + + logger.i { "Trying to open a file $fileName" } val file = share?.openFile( fileName, setOf(AccessMask.GENERIC_ALL), fileAttributes, SMB2ShareAccess.ALL, - SMB2CreateDisposition.FILE_OVERWRITE, + SMB2CreateDisposition.FILE_OVERWRITE_IF, createOptions ) file?.let { + logger.i { "Got the file, writing contents" } val oStream: OutputStream = it.outputStream oStream.write(contents.toByteArray()) oStream.flush() oStream.close() + logger.i { "Got the file, writing contents" } + it.close() return true } - } catch (_: Exception) { + } catch (e: Exception) { + logger.e(e) { "Failed to save to file" } } return false } From bffb9020e93a595ed63e5683d7fc361bc19901ab Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 14 Feb 2024 09:10:36 -0500 Subject: [PATCH 5/5] fixing tests --- .../datasources/ImageMetadataDataSourceTest.kt | 2 +- .../datasources/PlaylistSQLDataSourceTest.kt | 16 ++++++++++------ .../ui/viewmodel/ImageViewModelTest.kt | 12 ++++++------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSourceTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSourceTest.kt index 5cb0eb58..15589d1a 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSourceTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageMetadataDataSourceTest.kt @@ -46,7 +46,7 @@ class ImageMetadataDataSourceTest { val newMetadataFileDetails = MetadataFileDetails( "MyPath.png", - listOf("Tag1", "Wallpaper") + setOf("Tag1", "Wallpaper"), ) val mutableFiles = metadata.files.toMutableList() mutableFiles.add(newMetadataFileDetails) diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSourceTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSourceTest.kt index 29129d34..43fbb520 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSourceTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSourceTest.kt @@ -18,24 +18,28 @@ Testing [PlaylistSQLDataSource] class PlaylistSQLDataSourceTest { private val imageDirectory = ImageDirectory( - DefaultNetworkDirectoryDetails( + details = DefaultNetworkDirectoryDetails( fullPath = "Image1.png", id = 1, - ) + ), + metaData = null, + ) private val imageDirectoryList: List = listOf( imageDirectory, ImageDirectory( - DefaultNetworkDirectoryDetails( + details = DefaultNetworkDirectoryDetails( fullPath = "Photos/Image2.png", id = 2, - ) + ), + metaData = null, ), ImageDirectory( - DefaultNetworkDirectoryDetails( + details = DefaultNetworkDirectoryDetails( fullPath = "Image3.png", id = 3, - ) + ), + metaData = null, ), ) diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/ImageViewModelTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/ImageViewModelTest.kt index 9955aa21..21248ae9 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/ImageViewModelTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/ImageViewModelTest.kt @@ -31,12 +31,12 @@ class ImageViewModelTest : KoinTest { private val directories = listOf( - ImageDirectory(DefaultNetworkDirectoryDetails("Peeng.png", 1)), - ImageDirectory(DefaultNetworkDirectoryDetails("Jaypeg.jpg", 2)), - ImageDirectory(DefaultNetworkDirectoryDetails("Photos/Peeng2.png", 3)), - ImageDirectory(DefaultNetworkDirectoryDetails("Photos/Jaypeg2.jpg", 4)), - ImageDirectory(DefaultNetworkDirectoryDetails("Photos/SubPhotos/Peeng3.png", 5)), - ImageDirectory(DefaultNetworkDirectoryDetails("Photos/SubPhotos/Jaypeg3.jpg", 6)), + ImageDirectory(DefaultNetworkDirectoryDetails("Peeng.png", 1), null), + ImageDirectory(DefaultNetworkDirectoryDetails("Jaypeg.jpg", 2), null), + ImageDirectory(DefaultNetworkDirectoryDetails("Photos/Peeng2.png", 3), null), + ImageDirectory(DefaultNetworkDirectoryDetails("Photos/Jaypeg2.jpg", 4), null), + ImageDirectory(DefaultNetworkDirectoryDetails("Photos/SubPhotos/Peeng3.png", 5), null), + ImageDirectory(DefaultNetworkDirectoryDetails("Photos/SubPhotos/Jaypeg3.jpg", 6), null), ) @BeforeTest