Skip to content

Commit

Permalink
Merge pull request #52 from KevinSchildhorn/FileMetadata
Browse files Browse the repository at this point in the history
File metadata
  • Loading branch information
KevinSchildhorn authored Feb 14, 2024
2 parents 5c37dd5 + bffb902 commit ece2b80
Show file tree
Hide file tree
Showing 27 changed files with 344 additions and 41 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ fun ConfirmationDialogPreview() {
@Composable
fun TextConfirmationDialogPreview() {
TextEntryDialog(
title = "",
initialValue = "",
{

},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -42,13 +44,14 @@ 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")) }
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")) }
Expand Down Expand Up @@ -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")) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,4 +21,5 @@ expect object UseCaseFactory {
val retrieveSlideshowFromPlaylistUseCase: RetrieveSlideshowFromPlaylistUseCase
val retrieveDirectoryContentsUseCase: RetrieveDirectoryContentsUseCase
val retrieveImageUseCase: RetrieveImageUseCase
val saveMetadataForPathUseCase: SaveMetadataForPathUseCase
}
Original file line number Diff line number Diff line change
Expand Up @@ -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})"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.kevinschildhorn.fotopresenter.data

import kotlinx.serialization.Serializable

@Serializable
data class MetadataDetails(
val files: MutableList<MetadataFileDetails>
)

@Serializable
data class MetadataFileDetails(
val filePath: String,
val tags: Set<String>,
) {
val tagsString: String
get() = tags.joinToString(", ")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 {
logger?.i { "Importing Metadata" }
networkHandler.getMetadata()?.let {
logger?.i { "Found Metadata" }
return Json.decodeFromString<MetadataDetails>(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)
} catch (e: Exception) {
logger?.e(e) { "Error Exporting Metadata" }
return false
}

logger?.i { "Successfully Exported Metadata" }
return true
}

suspend fun readMetadataFromFile(filePath: String): 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.toSet()
)
}
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<NetworkDirectoryDetails> = dataSource.getFolderDirectories(path)
val imageDirectories: List<NetworkDirectoryDetails> = dataSource.getImageDirectories(path)
val folderDirectories: List<NetworkDirectoryDetails> =
directoryDataSource.getFolderDirectories(path)
val imageDirectories: List<NetworkDirectoryDetails> =
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 }
)
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ class RetrieveSlideshowFromPlaylistUseCase(
fullPath = item.directoryPath
)
if (item.directoryPath.isImagePath) {
listOf(ImageDirectory(directoryDetails))
listOf(
ImageDirectory(
directoryDetails,
null
)
)
} else {
retrieveDirectoryUseCase(
directoryDetails = directoryDetails,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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<String> = tags.split(",").map { it.trim() }
val metaData = dataSource.importMetaData()

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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -51,6 +52,7 @@ enum class DirectoryOverlay {
LOGOUT_CONFIRMATION,
PLAYLIST,
FILTER,
METADATA,
NONE,
}

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ data class ImageDirectoryGridCellState(
) : DirectoryGridCellState {
override val actionSheetContexts: List<ActionSheetContext>
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)"
Expand Down
Loading

0 comments on commit ece2b80

Please sign in to comment.