diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt index ff21da3c..0452f5b1 100644 --- a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt @@ -7,12 +7,18 @@ import androidx.compose.ui.graphics.asImageBitmap import com.hierynomus.smbj.share.File actual fun getBitmapFromFile(file: File, size: Int): ImageBitmap? { + val options = BitmapFactory.Options() - options.inSampleSize = 2 - BitmapFactory.decodeStream(file.inputStream, null, options)?.let { - val dimensions = getScaledDimensions(it.width, it.height, size) - return Bitmap.createScaledBitmap(it, dimensions.first, dimensions.second, false) - .asImageBitmap() - } - return null + options.inJustDecodeBounds = true + BitmapFactory.decodeStream(file.inputStream, null, options) + + val height: Int = options.outHeight + val width: Int = options.outWidth + val dimensions = getScaledDimensions(width, height, size) + val heightRatio: Int = Math.round(height.toFloat() / dimensions.second.toFloat()) + val widthRatio: Int = Math.round(width.toFloat() / dimensions.first.toFloat()) + options.inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio + + options.inJustDecodeBounds = false + return BitmapFactory.decodeStream(file.inputStream, null, options)?.asImageBitmap() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt index 176503b2..cc2efaa8 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/image/RetrieveImageUseCase.kt @@ -14,73 +14,24 @@ class RetrieveImageUseCase( private val logger: Logger, ) { - private val IMAGE_SIZE_SMALL = 64 - private val IMAGE_SIZE_MEDIUM = 256 - private val IMAGE_SIZE_LARGE = 512 - private val IMAGE_SIZE_EXTRA_LARGE = 1024 - suspend operator fun invoke( directory: ImageDirectory, - callback: suspend (State) -> Unit, - ) { + imageSize: Int, + ): ImageBitmap? { val imageName = "\"${directory.details.fullPath}\"" logger.i { "Starting to get Image $imageName" } - callback(State.LOADING) - - var workingWidth: Int = 0 - var updatedImage: Boolean = false imageCacheDataSource.getImage(directory.details)?.let { logger.i { "$imageName found in cache, using that" } - callback(State.SUCCESS(it)) - workingWidth = it.width - } - logger.i { "Getting Image Bitmap from File $imageName" } - if (workingWidth <= IMAGE_SIZE_SMALL) { - logger.i { "Getting Small Bitmap for $imageName" } - - val smallImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_SMALL) - callback(smallImageBitmap.asState) - workingWidth = IMAGE_SIZE_SMALL - updatedImage = true - } - if (workingWidth <= IMAGE_SIZE_MEDIUM) { - logger.i { "Getting Medium Image" } - val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_MEDIUM) - callback(mediumImageBitmap.asState) - workingWidth = IMAGE_SIZE_MEDIUM - updatedImage = true + return it } - if (workingWidth <= IMAGE_SIZE_LARGE) { - logger.i { "Getting Large Image" } - val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_LARGE) - callback(mediumImageBitmap.asState) - workingWidth = IMAGE_SIZE_LARGE - updatedImage = true - } - if (workingWidth <= IMAGE_SIZE_EXTRA_LARGE || !updatedImage) { - logger.i { "Getting Extra Large Image" } - val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_EXTRA_LARGE) - callback(mediumImageBitmap.asState) - workingWidth = IMAGE_SIZE_EXTRA_LARGE - } - } - private fun downloadAndStoreImage( - directory: ImageDirectory, - bitmapSize: Int, - ): ImageBitmap? { logger.i { "Getting Image Bitmap from File ${directory.name}" } - val imageBitmap: ImageBitmap? = directory.image?.getImageBitmap(bitmapSize) + val imageBitmap: ImageBitmap? = directory.image?.getImageBitmap(imageSize) imageBitmap?.let { logger.i { "Caching new Image ${directory.name}" } imageCacheDataSource.saveImage(directory.details, it) } return imageBitmap } - - private val ImageBitmap?.asState: State - get() = this?.let { - State.SUCCESS(it) - } ?: State.ERROR("No Image Found") } 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 9e718b26..37f3400d 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 @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import com.kevinschildhorn.fotopresenter.UseCaseFactory +import kotlinx.coroutines.cancelChildren interface ImageViewModel { var scope: CoroutineScope? @@ -41,7 +42,6 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel private val _uiState = MutableStateFlow(ImageScreenState()) override var scope: CoroutineScope? = null override val imageUiState: StateFlow = _uiState.asStateFlow() - private val jobs: MutableList = mutableListOf() override fun setImageDirectories(directories: List) { _uiState.update { it.copy(imageDirectories = directories) } @@ -83,10 +83,7 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel override fun cancelImageJobs() { logger?.d { "Cancelling Image Jobs" } - jobs.forEach { - it.cancel() - } - jobs.clear() + scope?.coroutineContext?.cancelChildren() } private fun updateSelectedImage() { @@ -106,14 +103,9 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel scope?.launch(Dispatchers.Default) { val retrieveImagesUseCase = UseCaseFactory.retrieveImageUseCase logger?.d { "Retrieving Image" } - retrieveImagesUseCase(imageDirectory) { newState: State -> - logger?.d { "Image State Updated $newState" } - newState.value?.let { imageBitmap -> - _uiState.update { it.copy(selectedImage = imageBitmap) } - } - } - }?.let { - jobs.add(it) + + val imageBitmap = retrieveImagesUseCase(imageDirectory, imageSize = 1024) + _uiState.update { it.copy(selectedImage = imageBitmap) } } } } 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..0f19cdc9 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/screens/directory/DirectoryViewModel.kt @@ -1,14 +1,16 @@ package com.kevinschildhorn.fotopresenter.ui.screens.directory 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.ImageDirectory import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails import com.kevinschildhorn.fotopresenter.data.PlaylistDetails import com.kevinschildhorn.fotopresenter.data.State -import com.kevinschildhorn.fotopresenter.UseCaseFactory import com.kevinschildhorn.fotopresenter.data.network.NetworkHandlerException import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository +import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageUseCase import com.kevinschildhorn.fotopresenter.extension.addPath import com.kevinschildhorn.fotopresenter.extension.navigateBackToPathAtIndex import com.kevinschildhorn.fotopresenter.ui.SortingType @@ -17,13 +19,18 @@ import com.kevinschildhorn.fotopresenter.ui.screens.common.ActionSheetContext import com.kevinschildhorn.fotopresenter.ui.screens.common.DefaultImageViewModel import com.kevinschildhorn.fotopresenter.ui.screens.common.ImageViewModel import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancelChildren 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 kotlinx.coroutines.plus +import kotlinx.datetime.Clock import org.koin.core.component.KoinComponent class DirectoryViewModel( @@ -33,13 +40,16 @@ class DirectoryViewModel( ImageViewModel by DefaultImageViewModel(logger), KoinComponent { + private val slideshowScope: CoroutineScope = viewModelScope + Dispatchers.IO + private val imageScope: CoroutineScope = viewModelScope + Dispatchers.IO + private val _uiState = MutableStateFlow(DirectoryScreenState()) val uiState: StateFlow = _uiState.asStateFlow() private val _directoryContentsState = MutableStateFlow(DirectoryContents()) + // Indexes of all Downloaded images private val downloadedImageSet: MutableSet = mutableSetOf() - private val jobs: MutableList = mutableListOf() private val currentPath: String get() = uiState.value.currentPath @@ -48,7 +58,7 @@ class DirectoryViewModel( get() = uiState.value.selectedDirectory?.actionSheetContexts ?: emptyList() init { - setImageScope(viewModelScope) + setImageScope(viewModelScope + Dispatchers.Default) } fun refreshScreen() { @@ -84,13 +94,12 @@ class DirectoryViewModel( 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) { + slideshowScope.launch { 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!" } @@ -136,7 +145,7 @@ class DirectoryViewModel( logger.i { "Changing directory to path $path" } cancelJobs() - viewModelScope.launch(Dispatchers.Default) { + slideshowScope.launch(Dispatchers.Default) { val changeDirectoryUseCase = UseCaseFactory.changeDirectoryUseCase try { logger.i { "Getting New Path" } @@ -171,7 +180,7 @@ class DirectoryViewModel( private fun updateDirectories() { logger.i { "Updating Directories" } _uiState.update { it.copy(state = UiState.LOADING) } - val job = viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.Default) { val retrieveDirectoryUseCase = UseCaseFactory.retrieveDirectoryContentsUseCase logger.i { "Getting Directory Contents" } @@ -183,34 +192,45 @@ class DirectoryViewModel( logger.i { "Current State ${uiState.value.state}" } updatePhotos() } - jobs.add(job) } private fun updatePhotos() { val count = imageUiState.value.imageDirectories.count() downloadedImageSet.clear() - _uiState.update { it.copy(totalImageCount = count, currentImageCount = 0) } - - logger.i { "Updating Photos" } - imageUiState.value.imageDirectories.forEachIndexed { index, imageDirectory -> - val job = viewModelScope.launch(Dispatchers.Default) { - val retrieveImagesUseCase = UseCaseFactory.retrieveImageUseCase - - retrieveImagesUseCase(imageDirectory) { newState -> - - downloadedImageSet.add(index) - _uiState.update { - it.copyImageState( - imageDirectory.id, - state = newState, - ).copy( - currentImageCount = downloadedImageSet.size - ) + imageScope.launch { + val startTime = Clock.System.now().toEpochMilliseconds() + logger.i { "Updating Photos" } + val retrieveImagesUseCase: RetrieveImageUseCase = UseCaseFactory.retrieveImageUseCase + val imageDirectories: List = imageUiState.value.imageDirectories + imageDirectories.mapIndexed{ index, imageDirectory -> + async { + retrieveImagesUseCase( + imageDirectory, + imageSize = 512, // TODO: Change + )?.let { newImage -> + logger.i { "Downloaded Image at index $index" } + downloadedImageSet.add(index) + + _uiState.update { + it.copyImageState( + imageDirectory.id, + state = State.SUCCESS(newImage), + ).copy( + currentImageCount = downloadedImageSet.size + ) + } + + if (_uiState.value.currentImageCount == _uiState.value.totalImageCount) { + val endTime = Clock.System.now().toEpochMilliseconds() + val difference: Float = (endTime.toFloat() - startTime.toFloat()) / 1000 + logger.i { "Downloading all images took $difference seconds" } + } } } - } - jobs.add(job) + }.awaitAll() + + // TODO: STORE LARGEST IMAGES } } @@ -279,10 +299,8 @@ class DirectoryViewModel( private fun cancelJobs() { logger.d { "Cancelling Jobs!" } cancelImageJobs() - jobs.forEach { - it.cancel() - } - jobs.clear() + slideshowScope.coroutineContext.cancelChildren() + imageScope.coroutineContext.cancelChildren() logger.v { "Finished Cancelling Jobs!" } } 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 2930cc79..d920ed73 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 @@ -1,5 +1,6 @@ package com.kevinschildhorn.fotopresenter.data.network +import co.touchlab.kermit.Logger import com.hierynomus.msdtyp.AccessMask import com.hierynomus.msfscc.FileAttributes import com.hierynomus.mssmb2.SMB2CreateDisposition @@ -25,7 +26,8 @@ object SMBJHandler : NetworkHandler { private var connection: Connection? = null private var session: Session? = null private var share: DiskShare? = null - private val metaDataName: String = "FotoMetaData.json" + private const val metaDataName: String = "FotoMetaData.json" + private val logger = Logger.withTag("SMBJHandler") private val accessMask: Set = setOf( @@ -52,11 +54,13 @@ object SMBJHandler : NetworkHandler { val session: Session? = connection?.authenticate(context) share = session?.connectShare(credentials.sharedFolder) as? DiskShare if (share == null) { + logger.e { "Failed To Connect, shared was null" } disconnect() return false } } } catch (e: Exception) { + logger.e(e) { "Failed To Connect" } disconnect() return false }