From 2cf2304a69853729cc4a0716e8ecef285eda4c4a Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Sun, 31 Dec 2023 20:26:38 -0500 Subject: [PATCH 1/4] Adding Json Serialization --- build.gradle.kts | 1 + shared/build.gradle.kts | 2 + .../com/kevinschildhorn/fotopresenter/Koin.kt | 8 +- .../fotopresenter/data/PlaylistDetails.kt | 23 ++++- .../datasources/PlaylistFileDataSource.kt | 42 +++++++++ ...DataSource.kt => PlaylistSQLDataSource.kt} | 31 ++++-- .../data/network/NetworkHandler.kt | 8 ++ .../data/repositories/PlaylistRepository.kt | 51 ++++++---- .../RetrieveSlideshowFromPlaylistUseCase.kt | 6 +- .../ui/screens/playlist/PlaylistScreen.kt | 2 +- .../ui/screens/playlist/PlaylistViewModel.kt | 40 +++++--- .../datasources/PlaylistDataSourceTest.kt | 28 +++--- .../fotopresenter/UseCaseFactoryDesktop.kt | 19 ++-- .../fotopresenter/data/network/SMBJHandler.kt | 94 +++++++++++++++---- 14 files changed, 264 insertions(+), 91 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSource.kt rename shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/{PlaylistDataSource.kt => PlaylistSQLDataSource.kt} (77%) diff --git a/build.gradle.kts b/build.gradle.kts index 3d62306f..28722ab6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,4 +8,5 @@ plugins { id("org.jlleitschuh.gradle.ktlint").version("12.0.2").apply(false) id("dev.icerock.mobile.multiplatform-resources").version("0.23.0").apply(false) id("app.cash.sqldelight").version("2.0.1").apply(false) + kotlin("plugin.serialization").version("1.9.21").apply(false) } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index c97616e8..7a72cdde 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("org.jlleitschuh.gradle.ktlint") id("dev.icerock.mobile.multiplatform-resources") id("app.cash.sqldelight") + kotlin("plugin.serialization") } kotlin { @@ -36,6 +37,7 @@ kotlin { implementation("io.github.reactivecircus.cache4k:cache4k:0.12.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") implementation("io.insert-koin:koin-core:3.4.0") implementation("androidx.security:security-crypto:1.1.0-alpha06") implementation("co.touchlab:kermit:1.2.2") diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt index b267cc4d..a28cfaab 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt @@ -6,7 +6,8 @@ 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.datasources.PlaylistFileDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistSQLDataSource import com.kevinschildhorn.fotopresenter.data.repositories.CredentialsRepository import com.kevinschildhorn.fotopresenter.data.repositories.DirectoryRepository import com.kevinschildhorn.fotopresenter.data.repositories.ImageRepository @@ -43,8 +44,9 @@ val commonModule = single { ImageRemoteDataSource(get()) } single { ImageRepository(get()) } single { ImageCacheDataSource(get(), get(), baseLogger.withTag("ImageCacheDataSource")) } - single { PlaylistDataSource(get(), baseLogger.withTag("PlaylistDataSource")) } - single { PlaylistRepository(get()) } + single { PlaylistFileDataSource(baseLogger.withTag("PlaylistDataSource"), get()) } + single { PlaylistSQLDataSource(get(), baseLogger.withTag("PlaylistDataSource")) } + single { PlaylistRepository(get(), get()) } // Domain factory { ConnectToServerUseCase(get(), baseLogger.withTag("ConnectToServerUseCase")) } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/PlaylistDetails.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/PlaylistDetails.kt index dec2da8c..98972ffe 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/PlaylistDetails.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/PlaylistDetails.kt @@ -1,14 +1,14 @@ package com.kevinschildhorn.fotopresenter.data import com.kevinschildhorn.fotopresenter.PlaylistItems -import com.kevinschildhorn.fotopresenter.data.network.DefaultNetworkDirectoryDetails -import com.kevinschildhorn.fotopresenter.extension.isImagePath +import kotlinx.serialization.Serializable +@Serializable data class PlaylistDetails( val id: Long, val name: String, - val items: List = emptyList(), -){ + val items: List = emptyList(), +) { override fun toString(): String { return """ Playlist Details: @@ -17,4 +17,19 @@ data class PlaylistDetails( images: ${items.count()} """ } +} + +@Serializable +data class PlaylistItem( + val id: Long, + val playlistId: Long, + val directoryPath: String, + val directoryId: Long, +) { + constructor(item: PlaylistItems) : this( + id = item.id, + playlistId = item.playlist_id, + directoryPath = item.directory_path, + directoryId = item.directory_id + ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSource.kt new file mode 100644 index 00000000..4ac20215 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSource.kt @@ -0,0 +1,42 @@ +package com.kevinschildhorn.fotopresenter.data.datasources + +import co.touchlab.kermit.Logger +import com.kevinschildhorn.fotopresenter.data.PlaylistDetails +import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.koin.core.component.KoinComponent + +class PlaylistFileDataSource( + private val logger: Logger, + private val networkHandler: NetworkHandler, +) : KoinComponent { + + suspend fun importPlaylists(): List = + networkHandler.getPlaylists().mapNotNull { + try { + Json.decodeFromString(it) + } catch (e: Exception) { + null + } + } + + suspend fun deletePlaylist(playlist: PlaylistDetails): Boolean = + try { + networkHandler.deletePlaylist(playlist.name) + true + } catch (e: Exception) { + false + } + + suspend fun exportPlaylist(playlist: PlaylistDetails): Boolean { + try { + val jsonString = Json.encodeToString(playlist) + networkHandler.savePlaylist(playlist.name, jsonString) + } catch (e: Exception) { + logger.e(e) { "Error Exporting Playlists" } + return false + } + return true + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSource.kt similarity index 77% rename from shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSource.kt rename to shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSource.kt index 314919e7..270739f8 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSource.kt @@ -8,9 +8,10 @@ import com.kevinschildhorn.fotopresenter.PlaylistItems import com.kevinschildhorn.fotopresenter.data.Directory import com.kevinschildhorn.fotopresenter.data.ImageDirectory import com.kevinschildhorn.fotopresenter.data.PlaylistDetails +import com.kevinschildhorn.fotopresenter.data.PlaylistItem import org.koin.core.component.KoinComponent -class PlaylistDataSource( +class PlaylistSQLDataSource( driver: SqlDriver, private val logger: Logger? = null, ) : KoinComponent { @@ -35,10 +36,10 @@ class PlaylistDataSource( fun getAllPlaylists(): List { return try { - database.playlistQueries.selectAllPlaylists().executeAsList().map { + database.playlistQueries.selectAllPlaylists().executeAsList().map { playlist -> val images = - database.playlistItemsQueries.selectPlaylistImages(it.id).executeAsList() - PlaylistDetails(it.id,it.name, images) + database.playlistItemsQueries.selectPlaylistImages(playlist.id).executeAsList() + PlaylistDetails(playlist.id, playlist.name, images.map { PlaylistItem(it) }) } } catch (e: Exception) { emptyList() @@ -53,13 +54,27 @@ class PlaylistDataSource( val images = database.playlistItemsQueries.selectPlaylistImages(playList.id).executeAsList() logger?.i { "Retrieved Playlist!" } - PlaylistDetails(playList.id, playList.name, images) + PlaylistDetails(playList.id, playList.name, images.map { PlaylistItem(it) }) } catch (e: Exception) { null } } - fun insertPlaylistImage(playlistId: Long, directory: Directory): PlaylistItems? { + fun getPlaylistById(id: Long): PlaylistDetails? { + return try { + logger?.i { "Selecting playlist by id $id" } + val playList: Playlist = + database.playlistQueries.selectPlaylistById(id).executeAsOne() + val images = + database.playlistItemsQueries.selectPlaylistImages(playList.id).executeAsList() + logger?.i { "Retrieved Playlist!" } + PlaylistDetails(playList.id, playList.name, images.map { PlaylistItem(it) }) + } catch (e: Exception) { + null + } + } + + fun insertPlaylistImage(playlistId: Long, directory: Directory): PlaylistItem? { logger?.i { "Inserting Playlist Image ${directory.name}" } database.playlistItemsQueries.insertPlaylistImage( playlist_id = playlistId, @@ -69,14 +84,14 @@ class PlaylistDataSource( return getPlaylistImage(playlistId, directory.details.fullPath) } - fun getPlaylistImage(playlistId: Long, directoryPath: String): PlaylistItems? { + fun getPlaylistImage(playlistId: Long, directoryPath: String): PlaylistItem? { return try { logger?.i { "Selecting Playlist Image $playlistId" } val image: PlaylistItems = database.playlistItemsQueries.selectPlaylistImage(playlistId, directoryPath) .executeAsOne() logger?.i { "Selecting Playlist Image" } - image + PlaylistItem(image) } catch (e: Exception) { null } 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 060d17ba..84d31ba4 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 @@ -19,6 +19,14 @@ interface NetworkHandler { suspend fun openImage(path: String): SharedImage? suspend fun folderExists(path: String): Boolean? + + suspend fun savePlaylist(playlistName:String, json:String): Boolean + suspend fun getPlaylists(): List + + suspend fun setMetadata(json:String): Boolean + suspend fun getMetadata(): String? + + suspend fun deletePlaylist(playlistName: String) } class NetworkHandlerException : Exception { diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/PlaylistRepository.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/PlaylistRepository.kt index 14e05a87..60dcb40d 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/PlaylistRepository.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/PlaylistRepository.kt @@ -1,31 +1,44 @@ package com.kevinschildhorn.fotopresenter.data.repositories import com.kevinschildhorn.fotopresenter.Playlist -import com.kevinschildhorn.fotopresenter.PlaylistItems import com.kevinschildhorn.fotopresenter.data.Directory import com.kevinschildhorn.fotopresenter.data.ImageDirectory import com.kevinschildhorn.fotopresenter.data.PlaylistDetails -import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistDataSource +import com.kevinschildhorn.fotopresenter.data.PlaylistItem +import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistFileDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistSQLDataSource class PlaylistRepository( - private val playlistDataSource: PlaylistDataSource, + private val playlistSQLDataSource: PlaylistSQLDataSource, + private val playlistFileDataSource: PlaylistFileDataSource, ) { - fun createPlaylist(name: String, directories: List = emptyList()): Playlist? = - playlistDataSource.createPlaylist(name, directories) + suspend fun createPlaylist(name: String, directories: List = emptyList()): Playlist? { + val playlist = playlistSQLDataSource.createPlaylist(name, directories) + playlistSQLDataSource.getPlaylistByName(name)?.let { + playlistFileDataSource.exportPlaylist(it) + } + return playlist + } + + suspend fun getAllPlaylists(): List { + playlistFileDataSource.importPlaylists() + return playlistSQLDataSource.getAllPlaylists() + } + + suspend fun insertPlaylistImage(playlistId: Long, directory: Directory): PlaylistItem? { + val item = playlistSQLDataSource.insertPlaylistImage(playlistId, directory) + playlistSQLDataSource.getPlaylistById(playlistId)?.let { + playlistFileDataSource.exportPlaylist(it) + } + return item + } + + suspend fun deletePlaylist(id: Long): Boolean { + playlistSQLDataSource.getPlaylistById(id)?.let { + playlistFileDataSource.deletePlaylist(it) + } + return playlistSQLDataSource.deletePlaylist(id) + } - fun getAllPlaylists(): List = - playlistDataSource.getAllPlaylists() - - fun getPlaylistByName(name: String): PlaylistDetails? = - playlistDataSource.getPlaylistByName(name) - - fun insertPlaylistImage(playlistId: Long, directory: Directory): PlaylistItems? = - playlistDataSource.insertPlaylistImage(playlistId, directory) - - fun getPlaylistImage(playlistId: Long, directoryPath: String): PlaylistItems? = - playlistDataSource.getPlaylistImage(playlistId, directoryPath) - - fun deletePlaylist(id: Long): Boolean = - playlistDataSource.deletePlaylist(id) } \ 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 586cf76b..a3d2c77b 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 @@ -22,10 +22,10 @@ class RetrieveSlideshowFromPlaylistUseCase( logger.i { "Starting to get details from playlist ${playlistDetails.name}" } val directories: List = playlistDetails.items.map { item -> val directoryDetails = DefaultNetworkDirectoryDetails( - id = item.directory_id.toInt(), - fullPath = item.directory_path + id = item.directoryId.toInt(), + fullPath = item.directoryPath ) - if (item.directory_path.isImagePath) { + if (item.directoryPath.isImagePath) { listOf(ImageDirectory(directoryDetails)) } else { retrieveDirectoryUseCase( 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 50776824..34ed1cc1 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 @@ -91,7 +91,7 @@ fun PlaylistScreen( PlaylistDialog.DETAILS -> { uiState.selectedPlaylist?.let { - TextListDialog(it.items.map { it.directory_path }) { + TextListDialog(it.items.map { it.directoryPath }) { dialogOpen = PlaylistDialog.NONE } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/PlaylistViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/PlaylistViewModel.kt index 239a4c32..bfaf1f2e 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/PlaylistViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/playlist/PlaylistViewModel.kt @@ -7,10 +7,12 @@ import com.kevinschildhorn.fotopresenter.data.ImageDirectory import com.kevinschildhorn.fotopresenter.data.PlaylistDetails import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository import com.kevinschildhorn.fotopresenter.ui.shared.ViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent open class PlaylistViewModel( @@ -26,8 +28,10 @@ open class PlaylistViewModel( } fun refreshPlaylists() { - val allPlaylists = playlistRepository.getAllPlaylists() - _uiState.update { it.copy(playlists = allPlaylists) } + viewModelScope.launch(Dispatchers.Default) { + val allPlaylists = playlistRepository.getAllPlaylists() + _uiState.update { it.copy(playlists = allPlaylists) } + } } fun setSelectedPlaylist(id: Long) { @@ -42,27 +46,33 @@ open class PlaylistViewModel( } fun createPlaylist(name: String) { - playlistRepository.createPlaylist(name) - refreshPlaylists() + viewModelScope.launch(Dispatchers.Default) { + playlistRepository.createPlaylist(name) + refreshPlaylists() + } } fun addToPlaylist(directory: Directory, playlist: PlaylistDetails) { logger.i { "Inserting Playlist Image ${playlist.id} as ${directory.name}" } - playlistRepository.insertPlaylistImage( - playlistId = playlist.id, - directory = directory - )?.let { - logger.i { "Successfully inserted playlist image" } - } ?: run { - logger.w { "Failed to insert playlist image" } + viewModelScope.launch(Dispatchers.Default) { + playlistRepository.insertPlaylistImage( + playlistId = playlist.id, + directory = directory + )?.let { + logger.i { "Successfully inserted playlist image" } + } ?: run { + logger.w { "Failed to insert playlist image" } + } } } fun deletePlaylist() { - _uiState.value.selectedId?.let { - playlistRepository.deletePlaylist(it) + viewModelScope.launch(Dispatchers.Default) { + _uiState.value.selectedId?.let { + playlistRepository.deletePlaylist(it) + } + refreshPlaylists() + clearSelectedPlaylist() } - refreshPlaylists() - clearSelectedPlaylist() } } \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt index 32ea9e2d..2eafc626 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt @@ -13,7 +13,7 @@ import kotlin.test.assertTrue import kotlin.test.fail /** -Testing [PlaylistDataSource] +Testing [PlaylistSQLDataSource] **/ class PlaylistDataSourceTest { @@ -41,7 +41,7 @@ class PlaylistDataSourceTest { @Test fun `Create Playlist Success`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val playlist = dataSource.createPlaylist("Playlist1") assertEquals("Playlist1", playlist?.name) assertEquals(1, playlist?.id) @@ -49,7 +49,7 @@ class PlaylistDataSourceTest { @Test fun `Create Playlist Large`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val playlist = dataSource.createPlaylist("Playlist1", imageDirectoryList) assertEquals("Playlist1", playlist?.name) assertEquals(1, playlist?.id) @@ -61,7 +61,7 @@ class PlaylistDataSourceTest { @Test fun `Create Playlist Failure`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val playlist1 = dataSource.createPlaylist("Playlist1") val playlist2 = dataSource.createPlaylist("Playlist1") assertNotNull(playlist1) @@ -70,7 +70,7 @@ class PlaylistDataSourceTest { @Test fun `Insert Playlist Image Success`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) dataSource.createPlaylist("Playlist1")?.let { playlist -> val image = dataSource.insertPlaylistImage(playlist.id, imageDirectory) assertEquals(playlist.id, image?.playlist_id) @@ -82,7 +82,7 @@ class PlaylistDataSourceTest { @Test fun `Insert Playlist Image Failure`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) dataSource.createPlaylist("Playlist1")?.let { playlist -> val image1 = dataSource.insertPlaylistImage(playlist.id, imageDirectory) val image2 = dataSource.insertPlaylistImage(playlist.id, imageDirectory) @@ -95,7 +95,7 @@ class PlaylistDataSourceTest { @Test fun `Get Playlist by Name Success`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val playlistName = "Playlist1" val playlist = dataSource.createPlaylist(playlistName) val selectedPlaylist = dataSource.getPlaylistByName(playlistName) @@ -105,14 +105,14 @@ class PlaylistDataSourceTest { @Test fun `Select Playlist by Name Failure`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val selectedPlaylist = dataSource.getPlaylistByName("NonExistant") assertNull(selectedPlaylist) } @Test fun `Get Playlist Image Success`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) dataSource.createPlaylist("Playlist1")?.let { playlist -> val image1 = dataSource.insertPlaylistImage(playlist.id, imageDirectory) val image2 = dataSource.getPlaylistImage(playlist.id, imageDirectory.details.fullPath) @@ -128,14 +128,14 @@ class PlaylistDataSourceTest { @Test fun `Get Playlist Image Failure`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val image = dataSource.getPlaylistImage(0, imageDirectory.details.fullPath) assertNull(image) } @Test fun `Delete Playlist Success`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val image = dataSource.getPlaylistImage(0, imageDirectory.details.fullPath) assertNull(image) } @@ -143,7 +143,7 @@ class PlaylistDataSourceTest { @Test fun `Delete Playlist Failure`() { val playlistName = "Playlist1" - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val playlist = dataSource.createPlaylist(playlistName, imageDirectoryList) assertNotNull(playlist) @@ -164,7 +164,7 @@ class PlaylistDataSourceTest { @Test fun `Select All Playlists Success`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val playlist = dataSource.createPlaylist("Playlist1", imageDirectoryList) val playlists = dataSource.getAllPlaylists() assertEquals(1, playlists.count()) @@ -172,7 +172,7 @@ class PlaylistDataSourceTest { @Test fun `Select All Playlists Failure`() { - val dataSource = PlaylistDataSource(createInMemorySqlDriver()) + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) val playlists = dataSource.getAllPlaylists() assertEquals(0, playlists.count()) diff --git a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt index ffc53520..4aec8c21 100644 --- a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt +++ b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/UseCaseFactoryDesktop.kt @@ -6,7 +6,8 @@ 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.datasources.PlaylistFileDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistSQLDataSource import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler import com.kevinschildhorn.fotopresenter.data.network.SMBJHandler import com.kevinschildhorn.fotopresenter.data.repositories.CredentialsRepository @@ -25,7 +26,6 @@ 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 { @@ -38,16 +38,21 @@ actual object UseCaseFactory { networkHandler, baseLogger.withTag("DirectoryDataSource") ) + private val sqlDriver = DriverFactory().createDriver() private val credentialDataSource = CredentialsDataSource(settings) val credentialsRepository = CredentialsRepository(credentialDataSource) private val directoryRepository = DirectoryRepository(directoryDataSource) private val imageRepository = ImageRepository(ImageRemoteDataSource(networkHandler)) - private val playlistDataSource = PlaylistDataSource( - DriverFactory().createDriver(), + private val playlistSQLDataSource = PlaylistSQLDataSource( + sqlDriver, com.kevinschildhorn.fotopresenter.baseLogger ) - val playlistRepository = PlaylistRepository(playlistDataSource) - + private val playlistFileDataSource = + PlaylistFileDataSource( + baseLogger.withTag("playlistFileDataSource"), + networkHandler, + ) + val playlistRepository = PlaylistRepository(playlistSQLDataSource, playlistFileDataSource) actual val connectToServerUseCase: ConnectToServerUseCase get() = ConnectToServerUseCase( @@ -98,7 +103,7 @@ actual object UseCaseFactory { get() = RetrieveImageUseCase( imageCacheDataSource = ImageCacheDataSource( cache = SharedCache, - driver = DriverFactory().createDriver(), + driver = sqlDriver, logger = baseLogger.withTag("ImageCacheDataSource") ), logger = baseLogger.withTag("RetrieveImageUseCase") 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 5e1309e4..2930cc79 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 @@ -10,15 +10,22 @@ import com.hierynomus.smbj.auth.AuthenticationContext import com.hierynomus.smbj.connection.Connection import com.hierynomus.smbj.session.Session import com.hierynomus.smbj.share.DiskShare +import com.hierynomus.smbj.share.File import com.kevinschildhorn.fotopresenter.data.LoginCredentials import com.kevinschildhorn.fotopresenter.extension.addPath import com.kevinschildhorn.fotopresenter.ui.shared.SharedImage +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import java.io.OutputStream +import java.util.* + object SMBJHandler : NetworkHandler { private val client = SMBClient() private var connection: Connection? = null private var session: Session? = null private var share: DiskShare? = null + private val metaDataName: String = "FotoMetaData.json" private val accessMask: Set = setOf( @@ -88,23 +95,10 @@ object SMBJHandler : NetworkHandler { return result?.path } - override suspend fun openImage(path: String): SharedImage? { - try { - share?.openFile( - path, - accessMask, - attributes, - shareAccesses, - createDisposition, - createOptions, - )?.let { - return SharedImage(it) - } - } catch (e: Exception) { - return null - } - return null - } + override suspend fun openImage(path: String): SharedImage? = + getFile(path)?.let { + SharedImage(it) + } ?: run { null } override suspend fun folderExists(path: String): Boolean? { return share?.folderExists(path) @@ -118,4 +112,70 @@ object SMBJHandler : NetworkHandler { session = null connection = null } + + override suspend fun savePlaylist(playlistName: String, json: String): Boolean = + writeFile(fileName = "$playlistName.json", contents = json) + + override suspend fun getPlaylists(): List = + getDirectoryContents("") + .filter { it.fileExtension == "json" } + .filter { !it.fileName.contains(metaDataName) } + .mapNotNull { getFile(it.fullPath) } + .map { it.inputStream.readAllBytes().decodeToString() } + + override suspend fun setMetadata(json: String): Boolean = + writeFile(fileName = metaDataName, contents = json) + + override suspend fun getMetadata(): String? = getFile(metaDataName)?.let { + it.inputStream.readAllBytes().decodeToString() + } + + override suspend fun deletePlaylist(playlistName: String) { + share?.rm("$playlistName.json") + } + + + private fun getFile( + path: String, + ): File? = + try { + share?.openFile( + path, + accessMask, + attributes, + shareAccesses, + createDisposition, + createOptions, + ) + } catch (e: Exception) { + null + } + + + private fun writeFile(fileName: String, contents: String): Boolean { + val fileAttributes: MutableSet = HashSet() + fileAttributes.add(FileAttributes.FILE_ATTRIBUTE_NORMAL) + val createOptions: MutableSet = HashSet() + createOptions.add(SMB2CreateOptions.FILE_RANDOM_ACCESS) + + try { + val file = share?.openFile( + fileName, + setOf(AccessMask.GENERIC_ALL), + fileAttributes, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OVERWRITE, + createOptions + ) + file?.let { + val oStream: OutputStream = it.outputStream + oStream.write(contents.toByteArray()) + oStream.flush() + oStream.close() + return true + } + } catch (_: Exception) { + } + return false + } } From cfc4aa045e86b8b0c77f550db013039a14ac26e3 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Mon, 1 Jan 2024 11:15:13 -0500 Subject: [PATCH 2/4] Fixing Tests --- shared/build.gradle.kts | 1 + .../fotopresenter/data/LoginCredentials.kt | 10 ++ .../data/datasources/DirectoryDataSource.kt | 12 +- .../connection/ConnectToServerUseCase.kt | 2 +- .../ui/screens/common/ImageViewModel.kt | 1 + .../screens/directory/DirectoryViewModel.kt | 8 +- .../ui/screens/login/LoginViewModel.kt | 10 +- .../kevinschildhorn/fotopresenter/KoinTest.kt | 10 ++ .../datasources/DirectoryDataSourceTest.kt | 2 +- .../datasources/PlaylistDataSourceTest.kt | 10 +- .../data/network/MockNetworkHandler.kt | 107 +++++++++++++----- .../ui/viewmodel/DirectoryViewModelTest.kt | 10 +- .../ui/viewmodel/LoginViewModelTest.kt | 12 +- 13 files changed, 139 insertions(+), 56 deletions(-) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 7a72cdde..94e35985 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -55,6 +55,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") implementation("io.insert-koin:koin-test:3.4.0") implementation("app.cash.turbine:turbine:1.0.0") + implementation("app.cash.sqldelight:sqlite-driver:2.0.1") } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/LoginCredentials.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/LoginCredentials.kt index 1a9d4444..15094e55 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/LoginCredentials.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/LoginCredentials.kt @@ -16,4 +16,14 @@ data class LoginCredentials( username.isNotBlank() && password.isNotBlank() && sharedFolder.isNotBlank() + + override fun toString(): String { + return """ + hostname: $hostname + username: $username + password: $password + sharedFolder: $sharedFolder + shouldAutoConnect: $shouldAutoConnect + """.trimIndent() + } } 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 86f6b6e7..18bd4800 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 @@ -12,19 +12,19 @@ Fetches Directory info from a NAS **/ class DirectoryDataSource( private val networkHandler: NetworkHandler, - private val logger: Logger, + 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}" } + 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?" } + logger?.i { "Does the directory exist?" } //val exists = networkHandler.folderExists(directoryName) - //logger.i { "Does the directory exist? $exists" } + //logger?.i { "Does the directory exist? $exists" } - logger.i { "Opening the directory..." } + 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/domain/connection/ConnectToServerUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/connection/ConnectToServerUseCase.kt index cb9e9994..4963912a 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/connection/ConnectToServerUseCase.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/connection/ConnectToServerUseCase.kt @@ -13,7 +13,7 @@ class ConnectToServerUseCase( ) { suspend operator fun invoke(credentials: LoginCredentials): Boolean = try { - logger.i { "Connecting to Client" } + logger.i { "Connecting to Client ${credentials.hostname}" } client.connect(credentials) } catch (e: Exception) { logger.e(e) { "Something went wrong" } 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 24408181..9e718b26 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 @@ -82,6 +82,7 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel } override fun cancelImageJobs() { + logger?.d { "Cancelling Image Jobs" } jobs.forEach { it.cancel() } 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 dc4d642a..54c0e78f 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 @@ -79,11 +79,15 @@ class DirectoryViewModel( fun startSlideshow() { 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) @@ -273,11 +277,13 @@ class DirectoryViewModel( } private fun cancelJobs() { - logger.i { "Cancelling Jobs!" } + logger.d { "Cancelling Jobs!" } cancelImageJobs() jobs.forEach { it.cancel() } jobs.clear() + logger.v { "Finished Cancelling Jobs!" } + } } 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 f102ac72..262cfff1 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 @@ -24,10 +24,10 @@ class LoginViewModel( val credentials = credentialsRepository.fetchCredentials() _uiState.update { it.copy( - hostname = "192.168.1.190",//credentials.hostname, - username = "kevin",//credentials.username, - password = "9E^54qFq^z",//credentials.password, - sharedFolder = "Photos",//credentials.sharedFolder, + hostname = credentials.hostname, + username = credentials.username, + password = credentials.password, + sharedFolder = credentials.sharedFolder, shouldAutoConnect = credentials.shouldAutoConnect, ) } @@ -71,7 +71,7 @@ class LoginViewModel( ) if (!result) { - logger.w { "Error Occurred" } + logger.w { "Error Occurred Connecting to Server" } _uiState.update { it.copy(state = UiState.ERROR("")) } return@launch } else { diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/KoinTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/KoinTest.kt index 295f6a49..13b5995e 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/KoinTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/KoinTest.kt @@ -1,5 +1,7 @@ package com.kevinschildhorn.fotopresenter +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import co.touchlab.kermit.Logger import co.touchlab.kermit.LoggerConfig import com.kevinschildhorn.fotopresenter.data.network.MockNetworkHandler @@ -14,4 +16,12 @@ fun testingModule(settings: MapSettings = MapSettings()) = module { single { MockNetworkHandler } single { settings } + + single { + val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + PlaylistDatabase.Schema.create(driver) + driver + } + + } + commonModule diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSourceTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSourceTest.kt index 3d7ab206..2d081894 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSourceTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/DirectoryDataSourceTest.kt @@ -15,7 +15,7 @@ Testing [DirectoryDataSource] **/ class DirectoryDataSourceTest { private val networkHandler: MockNetworkHandler = MockNetworkHandler - private val dataSource = DirectoryDataSource(networkHandler) + private val dataSource = DirectoryDataSource(networkHandler, null) @BeforeTest fun startTest() = diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt index 2eafc626..02c7ab2b 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt @@ -73,8 +73,8 @@ class PlaylistDataSourceTest { val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) dataSource.createPlaylist("Playlist1")?.let { playlist -> val image = dataSource.insertPlaylistImage(playlist.id, imageDirectory) - assertEquals(playlist.id, image?.playlist_id) - assertEquals(imageDirectory.details.fullPath, image?.directory_path) + assertEquals(playlist.id, image?.playlistId) + assertEquals(imageDirectory.details.fullPath, image?.directoryPath) } ?: run { fail("Didn't Create Playlist") } @@ -118,9 +118,9 @@ class PlaylistDataSourceTest { val image2 = dataSource.getPlaylistImage(playlist.id, imageDirectory.details.fullPath) assertNotNull(image1) assertNotNull(image2) - assertEquals(image1?.playlist_id, image2?.playlist_id) - assertEquals(image1?.id, image2?.id) - assertEquals(image1?.directory_path, image2?.directory_path) + assertEquals(image1.playlistId, image2.playlistId) + assertEquals(image1.id, image2.id) + assertEquals(image1.directoryPath, image2.directoryPath) } ?: run { fail("Didn't Create Playlist") } diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/network/MockNetworkHandler.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/network/MockNetworkHandler.kt index b7b0deec..b5cfd924 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/network/MockNetworkHandler.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/network/MockNetworkHandler.kt @@ -15,43 +15,51 @@ object MockNetworkHandler : NetworkHandler { val photoDirectoryId = 5 + private val playlists = mutableMapOf() + private var metadata: String? = null private val networkContents = mapOf( "" to - listOf( - DefaultNetworkDirectoryDetails(fullPath = "Photos", id = photoDirectoryId), - DefaultNetworkDirectoryDetails(fullPath = "NewDirectory", id = 1), - DefaultNetworkDirectoryDetails(fullPath = "Peeng.png", id = 75), - DefaultNetworkDirectoryDetails(fullPath = "Jaypeg.jpg", id = 3), - DefaultNetworkDirectoryDetails(fullPath = "textFile.txt", id = 4), - ), + listOf( + DefaultNetworkDirectoryDetails(fullPath = "Photos", id = photoDirectoryId), + DefaultNetworkDirectoryDetails(fullPath = "NewDirectory", id = 1), + DefaultNetworkDirectoryDetails(fullPath = "Peeng.png", id = 75), + DefaultNetworkDirectoryDetails(fullPath = "Jaypeg.jpg", id = 3), + DefaultNetworkDirectoryDetails(fullPath = "textFile.txt", id = 4), + ), "Directories" to - listOf( - DefaultNetworkDirectoryDetails(fullPath = "Directories/NewDirectory", id = 1), - DefaultNetworkDirectoryDetails(fullPath = "Directories/NewDirectory2", id = 2), - ), - "Photos" to - listOf( - DefaultNetworkDirectoryDetails(fullPath = "Photos/Peeng2.png", id = 2), - DefaultNetworkDirectoryDetails(fullPath = "Photos/Jaypeg2.jpg", id = 3), - DefaultNetworkDirectoryDetails(fullPath = "Photos/textFile2.txt", id = 4), - DefaultNetworkDirectoryDetails(fullPath = "Photos/SubPhotos", id = 5), - ), - "Photos/SubPhotos" to - listOf( - DefaultNetworkDirectoryDetails( - fullPath = "Photos/SubPhotos/Peeng3.png", - id = 2, + listOf( + DefaultNetworkDirectoryDetails( + fullPath = "Directories/NewDirectory", + id = 1 + ), + DefaultNetworkDirectoryDetails( + fullPath = "Directories/NewDirectory2", + id = 2 + ), ), - DefaultNetworkDirectoryDetails( - fullPath = "Photos/SubPhotos/Jaypeg3.jpg", - id = 3, + "Photos" to + listOf( + DefaultNetworkDirectoryDetails(fullPath = "Photos/Peeng2.png", id = 2), + DefaultNetworkDirectoryDetails(fullPath = "Photos/Jaypeg2.jpg", id = 3), + DefaultNetworkDirectoryDetails(fullPath = "Photos/textFile2.txt", id = 4), + DefaultNetworkDirectoryDetails(fullPath = "Photos/SubPhotos", id = 5), ), - DefaultNetworkDirectoryDetails( - fullPath = "Photos/SubPhotos/textFile3.txt", - id = 4, + "Photos/SubPhotos" to + listOf( + DefaultNetworkDirectoryDetails( + fullPath = "Photos/SubPhotos/Peeng3.png", + id = 2, + ), + DefaultNetworkDirectoryDetails( + fullPath = "Photos/SubPhotos/Jaypeg3.jpg", + id = 3, + ), + DefaultNetworkDirectoryDetails( + fullPath = "Photos/SubPhotos/textFile3.txt", + id = 4, + ), ), - ), ) private val successImageName: String = "Photos/Success.png" @@ -72,6 +80,12 @@ object MockNetworkHandler : NetworkHandler { } connected = credentials == successLoginCredentials print("Is Connected $connected\n") + if(!connected) { + print("Success Credentials: $successLoginCredentials\n") + print("Actual Credentials $credentials\n") + } + + credentials.toString() return connected } @@ -79,6 +93,17 @@ object MockNetworkHandler : NetworkHandler { connected = false } + override suspend fun getDirectoryDetails(path: String): NetworkDirectoryDetails? { + networkContents.values.forEach { details -> + details.find { detail -> + detail.fullPath == path + }?.let { + return it + } + } + return null + } + override suspend fun getDirectoryContents(path: String): List { print("Getting Directory Contents ${path}\n") @@ -100,4 +125,26 @@ object MockNetworkHandler : NetworkHandler { } return null } + + override suspend fun folderExists(path: String): Boolean? = + if (path == "") null + else networkContents.keys.contains(path) + + override suspend fun savePlaylist(playlistName: String, json: String): Boolean { + playlists[playlistName] = json + return true + } + + override suspend fun getPlaylists(): List = playlists.values.toList() + + override suspend fun setMetadata(json: String): Boolean { + metadata = json + return true + } + + override suspend fun getMetadata(): String? = metadata + + override suspend fun deletePlaylist(playlistName: String) { + playlists.remove(playlistName) + } } diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/DirectoryViewModelTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/DirectoryViewModelTest.kt index 6ca7e484..336393b7 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/DirectoryViewModelTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/DirectoryViewModelTest.kt @@ -5,6 +5,7 @@ import com.kevinschildhorn.fotopresenter.data.network.MockNetworkHandler import com.kevinschildhorn.fotopresenter.testingModule import com.kevinschildhorn.fotopresenter.ui.UiState import com.kevinschildhorn.fotopresenter.ui.screens.directory.DirectoryViewModel +import com.kevinschildhorn.fotopresenter.ui.screens.directory.FolderDirectoryGridCellState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking @@ -133,7 +134,12 @@ class DirectoryViewModelTest : KoinTest { state = awaitItem() assertEquals(UiState.SUCCESS, state.state) - viewModel.startSlideshow(MockNetworkHandler.photoDirectoryId) + val directory = FolderDirectoryGridCellState( + name = "", + id = MockNetworkHandler.photoDirectoryId, + ) + viewModel.setSelectedDirectory(directory) + viewModel.startSlideshow() while (state.slideshowDetails == null) { state = awaitItem() } @@ -159,6 +165,7 @@ class DirectoryViewModelTest : KoinTest { assertEquals(UiState.LOADING, state.state) state = awaitItem() assertEquals(UiState.SUCCESS, state.state) + cancelAndIgnoreRemainingEvents() } viewModel.imageUiState.test { viewModel.setSelectedImageById(75) @@ -167,6 +174,7 @@ class DirectoryViewModelTest : KoinTest { state = awaitItem() } assertEquals(0, state.selectedImageIndex) + cancelAndIgnoreRemainingEvents() } } } diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModelTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModelTest.kt index 931b3bf9..2fd40b5c 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModelTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModelTest.kt @@ -73,12 +73,12 @@ class LoginViewModelTest : KoinTest { startKoin { modules(testingModule(settings = settings)) with(viewModel.uiState.value) { - assertEquals(hostname, "defaultHostname") - assertEquals(username, "defaultUsername") - assertEquals(password, "defaultPassword") - assertEquals(sharedFolder, "") - assertEquals(shouldAutoConnect, false) - assertEquals(state, UiState.IDLE) + assertEquals("defaultHostname", hostname) + assertEquals("defaultUsername", username) + assertEquals("defaultPassword", password) + assertEquals("", sharedFolder) + assertEquals( false, shouldAutoConnect) + assertEquals(UiState.IDLE, state) } } } From b01a108c2936b20266c2323bec6c81b4be04d365 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Mon, 1 Jan 2024 12:35:47 -0500 Subject: [PATCH 3/4] adding tests for playlist data source --- .../datasources/PlaylistFileDataSource.kt | 6 +- .../datasources/PlaylistFileDataSourceTest.kt | 90 +++++++++++++++++++ ...ceTest.kt => PlaylistSQLDataSourceTest.kt} | 19 +++- .../data/network/MockNetworkHandler.kt | 23 ++++- 4 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSourceTest.kt rename shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/{PlaylistDataSourceTest.kt => PlaylistSQLDataSourceTest.kt} (90%) diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSource.kt index 4ac20215..38074427 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSource.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.json.Json import org.koin.core.component.KoinComponent class PlaylistFileDataSource( - private val logger: Logger, + private val logger: Logger?, private val networkHandler: NetworkHandler, ) : KoinComponent { @@ -17,6 +17,7 @@ class PlaylistFileDataSource( try { Json.decodeFromString(it) } catch (e: Exception) { + logger?.e(e) { "Error importing Playlist" } null } } @@ -26,6 +27,7 @@ class PlaylistFileDataSource( networkHandler.deletePlaylist(playlist.name) true } catch (e: Exception) { + logger?.e(e) { "Error Deleting Playlist" } false } @@ -34,7 +36,7 @@ class PlaylistFileDataSource( val jsonString = Json.encodeToString(playlist) networkHandler.savePlaylist(playlist.name, jsonString) } catch (e: Exception) { - logger.e(e) { "Error Exporting Playlists" } + logger?.e(e) { "Error Exporting Playlists" } return false } return true diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSourceTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSourceTest.kt new file mode 100644 index 00000000..ae1edf9e --- /dev/null +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistFileDataSourceTest.kt @@ -0,0 +1,90 @@ +package com.kevinschildhorn.fotopresenter.data.datasources + +import com.kevinschildhorn.fotopresenter.data.PlaylistDetails +import com.kevinschildhorn.fotopresenter.data.PlaylistItem +import com.kevinschildhorn.fotopresenter.data.network.MockNetworkHandler +import kotlinx.coroutines.runBlocking +import org.junit.Test +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** +Testing [PlaylistFileDataSource] + **/ +class PlaylistFileDataSourceTest { + private val networkHandler: MockNetworkHandler = MockNetworkHandler + + private val newPlaylist = PlaylistDetails( + id = 2, + name = "NewPlaylist", + items = listOf( + PlaylistItem( + id = 1, + playlistId = 2, + directoryPath = "Photos/SubPhotos/Peeng3.png", + directoryId = 2, + ), + PlaylistItem( + id = 2, + playlistId = 2, + directoryPath = "Photos/SubPhotos/Jaypeg3.jpg", + directoryId = 3, + ) + ), + ) + + @BeforeTest + fun startTest() = + runBlocking { + networkHandler.connectSuccessfully() + } + + @AfterTest + fun tearDown() = + runBlocking { + networkHandler.disconnect() + } + + @Test + fun `Import Playlist`() = runBlocking { + val dataSource = PlaylistFileDataSource(null, networkHandler) + val playlists = dataSource.importPlaylists() + + val existingPlaylist = playlists.firstOrNull() + assertNotNull(existingPlaylist) + assertEquals("Existing", existingPlaylist.name) + assertEquals(2, existingPlaylist.items.count()) + } + + @Test + fun `Export Playlist`() = runBlocking { + val dataSource = PlaylistFileDataSource(null, networkHandler) + var playlists = dataSource.importPlaylists().toMutableList() + assertEquals(1, playlists.count()) + + val result = dataSource.exportPlaylist(newPlaylist) + assertTrue(result) + + playlists = dataSource.importPlaylists().toMutableList() + + val searchedPlaylist = playlists.find { it.name == "NewPlaylist" } + assertNotNull(searchedPlaylist) + print("finished") + } + + @Test + fun `Delete Playlist`() = runBlocking { + val dataSource = PlaylistFileDataSource(null, networkHandler) + dataSource.exportPlaylist(newPlaylist) + var playlists = dataSource.importPlaylists() + val searchedPlaylist = playlists.find { it.name == "NewPlaylist" } + assertNotNull(searchedPlaylist) + dataSource.deletePlaylist(searchedPlaylist) + + playlists = dataSource.importPlaylists() + assertEquals(1, playlists.count()) + } +} \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSourceTest.kt similarity index 90% rename from shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt rename to shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSourceTest.kt index 02c7ab2b..29129d34 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistDataSourceTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/PlaylistSQLDataSourceTest.kt @@ -15,7 +15,7 @@ import kotlin.test.fail /** Testing [PlaylistSQLDataSource] **/ -class PlaylistDataSourceTest { +class PlaylistSQLDataSourceTest { private val imageDirectory = ImageDirectory( DefaultNetworkDirectoryDetails( @@ -110,6 +110,23 @@ class PlaylistDataSourceTest { assertNull(selectedPlaylist) } + @Test + fun `Get Playlist by Id Success`() { + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) + val playlistName = "Playlist1" + val playlist = dataSource.createPlaylist(playlistName) + val selectedPlaylist = dataSource.getPlaylistById(playlist?.id ?: -1) + assertEquals(playlist?.name, selectedPlaylist?.name) + assertEquals(playlist?.id, selectedPlaylist?.id) + } + + @Test + fun `Select Playlist by Id Failure`() { + val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) + val selectedPlaylist = dataSource.getPlaylistById(-1) + assertNull(selectedPlaylist) + } + @Test fun `Get Playlist Image Success`() { val dataSource = PlaylistSQLDataSource(createInMemorySqlDriver()) diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/network/MockNetworkHandler.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/network/MockNetworkHandler.kt index b5cfd924..591d2699 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/network/MockNetworkHandler.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/data/network/MockNetworkHandler.kt @@ -15,7 +15,28 @@ object MockNetworkHandler : NetworkHandler { val photoDirectoryId = 5 - private val playlists = mutableMapOf() + private val playlists = mutableMapOf( + "Existing" to """ + { + "id" : 1, + "name" : "Existing", + "items": [ + { + "id" : 1, + "playlistId" : 1, + "directoryPath" : "Photos/SubPhotos/Peeng3.png", + "directoryId" : 2 + }, + { + "id" : 2, + "playlistId" : 1, + "directoryPath" : "Photos/Jaypeg2.jpg", + "directoryId" : 3 + } + ] + } + """.trimIndent() + ) private var metadata: String? = null private val networkContents = mapOf( From 6594f0d5583296f95d8c1658d1c662151514c786 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Mon, 1 Jan 2024 12:56:13 -0500 Subject: [PATCH 4/4] updating tests --- .../fotopresenter/data/LoginCredentials.kt | 12 ++++--- .../ui/viewmodel/LoginViewModelTest.kt | 34 ++++++++++--------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/LoginCredentials.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/LoginCredentials.kt index 15094e55..3a450a43 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/LoginCredentials.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/LoginCredentials.kt @@ -19,11 +19,13 @@ data class LoginCredentials( override fun toString(): String { return """ - hostname: $hostname - username: $username - password: $password - sharedFolder: $sharedFolder - shouldAutoConnect: $shouldAutoConnect + LoginCredentials( + hostname: $hostname + username: $username + password: $password + sharedFolder: $sharedFolder + shouldAutoConnect: $shouldAutoConnect + ) """.trimIndent() } } diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModelTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModelTest.kt index 2fd40b5c..a02dd170 100644 --- a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModelTest.kt +++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModelTest.kt @@ -1,10 +1,12 @@ package com.kevinschildhorn.fotopresenter.ui.viewmodel +import app.cash.turbine.test import com.kevinschildhorn.fotopresenter.testingModule import com.kevinschildhorn.fotopresenter.ui.UiState import com.kevinschildhorn.fotopresenter.ui.screens.login.LoginViewModel import com.russhwolf.settings.MapSettings import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.koin.core.context.startKoin @@ -77,7 +79,7 @@ class LoginViewModelTest : KoinTest { assertEquals("defaultUsername", username) assertEquals("defaultPassword", password) assertEquals("", sharedFolder) - assertEquals( false, shouldAutoConnect) + assertEquals(false, shouldAutoConnect) assertEquals(UiState.IDLE, state) } } @@ -147,17 +149,17 @@ class LoginViewModelTest : KoinTest { viewModel.updateShouldAutoConnect(true) viewModel.login() - with(viewModel.uiState.value) { - assertEquals(UiState.LOADING, state) - } - - advanceUntilIdle() - with(viewModel.uiState.value) { - print(this.state) - // assertTrue(state is UiState.ERROR) TODO + viewModel.uiState.test { + var state = awaitItem() + while (state.state == UiState.LOADING || state.state == UiState.IDLE) { + state = awaitItem() + } + assertTrue(state.state is UiState.ERROR) + cancelAndIgnoreRemainingEvents() } } + @Test fun `Login Success`() = runTest { @@ -171,13 +173,13 @@ class LoginViewModelTest : KoinTest { viewModel.updateShouldAutoConnect(false) viewModel.login() - with(viewModel.uiState.value) { - assertEquals(UiState.LOADING, state) - } - - advanceUntilIdle() - with(viewModel.uiState.value) { - // assertEquals(UiState.SUCCESS, state) TODO + viewModel.uiState.test { + var state = awaitItem() + while (state.state == UiState.LOADING || state.state == UiState.IDLE) { + state = awaitItem() + } + assertEquals(UiState.SUCCESS, state.state) + cancelAndIgnoreRemainingEvents() } }