diff --git a/androidApp/src/androidMain/res/values/strings.xml b/androidApp/src/androidMain/res/values/strings.xml
index 592270bf..741d4431 100644
--- a/androidApp/src/androidMain/res/values/strings.xml
+++ b/androidApp/src/androidMain/res/values/strings.xml
@@ -1,3 +1,3 @@
- My application
+ FotoPresenter
\ No newline at end of file
diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig
index 2327a19a..f28f0fe4 100644
--- a/iosApp/Configuration/Config.xcconfig
+++ b/iosApp/Configuration/Config.xcconfig
@@ -1,3 +1,3 @@
TEAM_ID=
BUNDLE_ID=com.kevinschildhorn.fotopresenter
-APP_NAME=My application
+APP_NAME=Foto Presenter
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index bbf1698f..ddb54951 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -33,6 +33,8 @@ kotlin {
implementation(compose.components.resources)
implementation("br.com.devsrsouza.compose.icons:eva-icons:1.1.0")
+ implementation("io.github.reactivecircus.cache4k:cache4k:0.12.0")
+
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/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt
index 86a62a88..ff21da3c 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
@@ -14,6 +14,5 @@ actual fun getBitmapFromFile(file: File, size: Int): ImageBitmap? {
return Bitmap.createScaledBitmap(it, dimensions.first, dimensions.second, false)
.asImageBitmap()
}
-
return null
-}
+}
\ No newline at end of file
diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt
new file mode 100644
index 00000000..5646bc5a
--- /dev/null
+++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt
@@ -0,0 +1,29 @@
+package com.kevinschildhorn.fotopresenter.ui.shared
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import java.nio.ByteBuffer
+
+actual object SharedImageConverter {
+ actual fun convertBytes(byteArray: ByteArray): ImageBitmap {
+ val bmp = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
+ return bmp.asImageBitmap()
+ }
+
+ actual fun convertImage(imageBitmap: ImageBitmap): ByteArray {
+ return imageBitmap.asAndroidBitmap().convertToByteArray()
+ }
+
+ private fun Bitmap.convertToByteArray(): ByteArray {
+ val size = this.byteCount
+ val buffer = ByteBuffer.allocate(size)
+ val bytes = ByteArray(size)
+ this.copyPixelsToBuffer(buffer)
+ buffer.rewind()
+ buffer.get(bytes)
+ return bytes
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/App.kt b/shared/src/commonMain/kotlin/App.kt
index 21381d8f..3a951d83 100644
--- a/shared/src/commonMain/kotlin/App.kt
+++ b/shared/src/commonMain/kotlin/App.kt
@@ -19,9 +19,15 @@ fun App(
when (currentScreen.value) {
Screen.LOGIN ->
LoginScreen(loginViewModel) {
+ directoryViewModel.setLoggedIn()
currentScreen.value = Screen.DIRECTORY
}
- Screen.DIRECTORY -> DirectoryScreen(directoryViewModel)
+
+ Screen.DIRECTORY ->
+ DirectoryScreen(directoryViewModel) {
+ loginViewModel.setLoggedOut()
+ currentScreen.value = Screen.LOGIN
+ }
}
}
}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt
index 18c20576..4fd20e16 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt
@@ -11,7 +11,9 @@ import com.kevinschildhorn.fotopresenter.data.repositories.ImageRepository
import com.kevinschildhorn.fotopresenter.domain.AutoConnectUseCase
import com.kevinschildhorn.fotopresenter.domain.ChangeDirectoryUseCase
import com.kevinschildhorn.fotopresenter.domain.ConnectToServerUseCase
+import com.kevinschildhorn.fotopresenter.domain.LogoutUseCase
import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase
+import com.kevinschildhorn.fotopresenter.domain.RetrieveImagesUseCase
import com.kevinschildhorn.fotopresenter.domain.SaveCredentialsUseCase
import com.kevinschildhorn.fotopresenter.ui.viewmodel.DirectoryViewModel
import com.kevinschildhorn.fotopresenter.ui.viewmodel.LoginViewModel
@@ -35,6 +37,7 @@ val commonModule =
factory { ChangeDirectoryUseCase(get(), baseLogger.withTag("ChangeDirectoryUseCase")) }
factory { AutoConnectUseCase(get(), get(), baseLogger.withTag("AutoConnectUseCase")) }
factory { SaveCredentialsUseCase(get(), baseLogger.withTag("SaveCredentialsUseCase")) }
+ factory { LogoutUseCase(get(), get(), baseLogger.withTag("LogoutUseCase")) }
factory {
RetrieveDirectoryContentsUseCase(
get(),
@@ -42,6 +45,7 @@ val commonModule =
baseLogger.withTag("RetrieveDirectoryContentsUseCase"),
)
}
+ factory { RetrieveImagesUseCase(baseLogger.withTag("RetrieveImagesUseCase")) }
// UI
single { LoginViewModel(baseLogger.withTag("LoginViewModel"), get()) }
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/State.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/State.kt
index 96e86cc5..cf5cf657 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/State.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/State.kt
@@ -33,6 +33,9 @@ sealed class State {
return this
}
+ val value: DATA?
+ get() = (this as? SUCCESS)?.data
+
@Composable
fun asComposable(modifier: Modifier = Modifier) {
when (this) {
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt
new file mode 100644
index 00000000..b2944695
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/ImageCacheDataSource.kt
@@ -0,0 +1,19 @@
+package com.kevinschildhorn.fotopresenter.data.datasources
+
+import androidx.compose.ui.graphics.ImageBitmap
+import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails
+import com.kevinschildhorn.fotopresenter.ui.shared.SharedCache
+
+class ImageCacheDataSource {
+ fun getImage(directory: NetworkDirectoryDetails): ImageBitmap? = SharedCache.getImage(directory.cacheId)
+
+ fun saveImage(
+ directory: NetworkDirectoryDetails,
+ bitmap: ImageBitmap,
+ ) {
+ SharedCache.cacheImage(directory.cacheId, bitmap)
+ }
+
+ private val NetworkDirectoryDetails.cacheId: String
+ get() = "$name.$id"
+}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/CredentialsRepository.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/CredentialsRepository.kt
index 83e9c95c..5227449c 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/CredentialsRepository.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/CredentialsRepository.kt
@@ -31,4 +31,8 @@ class CredentialsRepository(
dataSource.sharedFolder = sharedFolder
dataSource.shouldAutoConnect = shouldAutoConnect
}
+
+ fun clearAutoConnect() {
+ dataSource.shouldAutoConnect = false
+ }
}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/LogoutUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/LogoutUseCase.kt
new file mode 100644
index 00000000..9ee1dd66
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/LogoutUseCase.kt
@@ -0,0 +1,19 @@
+package com.kevinschildhorn.fotopresenter.domain
+
+import co.touchlab.kermit.Logger
+import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler
+import com.kevinschildhorn.fotopresenter.data.repositories.CredentialsRepository
+
+/**
+Logging out of App
+ **/
+class LogoutUseCase(
+ private val credentialsRepository: CredentialsRepository,
+ private val networkHandler: NetworkHandler,
+ private val logger: Logger,
+) {
+ suspend operator fun invoke() {
+ networkHandler.disconnect()
+ credentialsRepository.clearAutoConnect()
+ }
+}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/RetrieveImagesUseCase.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/RetrieveImagesUseCase.kt
new file mode 100644
index 00000000..79e94b23
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/domain/RetrieveImagesUseCase.kt
@@ -0,0 +1,34 @@
+package com.kevinschildhorn.fotopresenter.domain
+
+import androidx.compose.ui.graphics.ImageBitmap
+import co.touchlab.kermit.Logger
+import com.kevinschildhorn.fotopresenter.data.ImageDirectory
+import com.kevinschildhorn.fotopresenter.data.State
+import com.kevinschildhorn.fotopresenter.ui.shared.SharedCache
+
+/**
+Retrieving Directories from Location
+ **/
+class RetrieveImagesUseCase(
+ private val logger: Logger,
+) {
+ suspend operator fun invoke(
+ directory: ImageDirectory,
+ callback: suspend (State) -> Unit,
+ ) {
+ callback(State.LOADING)
+ SharedCache.getImage(directory.name)?.let {
+ callback(State.SUCCESS(it))
+ }
+ val imageBitmap: ImageBitmap? = directory.image?.getImageBitmap(400)
+
+ callback(
+ imageBitmap?.let {
+ State.SUCCESS(it)
+ } ?: State.ERROR("No Image Found"),
+ )
+ imageBitmap?.let {
+ SharedCache.cacheImage(directory.name, it)
+ }
+ }
+}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/extension/ListExtension.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/extension/ListExtension.kt
new file mode 100644
index 00000000..267f395f
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/extension/ListExtension.kt
@@ -0,0 +1,19 @@
+package com.kevinschildhorn.fotopresenter.extension
+
+fun List.getNextIndex(index: Int): Int {
+ val nextIndex = index + 1
+ return if (nextIndex >= this.count()) {
+ 0
+ } else {
+ nextIndex
+ }
+}
+
+fun List.getPreviousIndex(index: Int): Int {
+ val previousIndex = index - 1
+ return if (previousIndex < 0) {
+ this.count() - 1
+ } else {
+ previousIndex
+ }
+}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/DirectoryScreen.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/DirectoryScreen.kt
index fa2239cd..248b7548 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/DirectoryScreen.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/DirectoryScreen.kt
@@ -7,18 +7,25 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import com.kevinschildhorn.fotopresenter.data.State
import com.kevinschildhorn.fotopresenter.ui.atoms.Padding
+import com.kevinschildhorn.fotopresenter.ui.compose.common.PrimaryButton
import com.kevinschildhorn.fotopresenter.ui.compose.directory.DirectoryGrid
import com.kevinschildhorn.fotopresenter.ui.viewmodel.DirectoryViewModel
@Composable
-fun DirectoryScreen(viewModel: DirectoryViewModel) {
+fun DirectoryScreen(
+ viewModel: DirectoryViewModel,
+ onLogout: () -> Unit,
+) {
LaunchedEffect(Unit) {
viewModel.refreshScreen()
}
val uiState by viewModel.uiState.collectAsState()
+ if (!uiState.loggedIn)
+ {
+ onLogout()
+ }
uiState.state.asComposable(
modifier =
Modifier.padding(
@@ -26,22 +33,33 @@ fun DirectoryScreen(viewModel: DirectoryViewModel) {
vertical = Padding.SMALL.dp,
),
)
+ PrimaryButton("Logout") {
+ viewModel.logout()
+ }
DirectoryGrid(
uiState.directoryGridState,
modifier =
Modifier
.padding(top = Padding.EXTRA_LARGE.dp),
onFolderPressed = {
- uiState.selectedImage = State.IDLE
viewModel.changeDirectory(it)
},
onImageDirectoryPressed = {
- uiState.selectedImage = it
+ viewModel.setSelectedImageById(it)
},
)
- if (uiState.selectedImage != State.IDLE) {
- ImagePreviewOverlay(uiState.selectedImage) {
- uiState.selectedImage = State.IDLE
- }
+ uiState.selectedImage?.let {
+ ImagePreviewOverlay(
+ it,
+ onDismiss = {
+ viewModel.clearPresentedImage()
+ },
+ onBack = {
+ viewModel.showPreviousImage()
+ },
+ onForward = {
+ viewModel.showNextImage()
+ },
+ )
}
}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/ImagePreviewOverlay.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/ImagePreviewOverlay.kt
index 4d7bb0a2..3c1bf2fb 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/ImagePreviewOverlay.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/ImagePreviewOverlay.kt
@@ -4,28 +4,37 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Text
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.dp
import com.kevinschildhorn.atomik.color.base.composeColor
-import com.kevinschildhorn.fotopresenter.data.State
import com.kevinschildhorn.fotopresenter.ui.atoms.FotoColors
import com.kevinschildhorn.fotopresenter.ui.atoms.Padding
import com.kevinschildhorn.fotopresenter.ui.compose.common.Overlay
+import compose.icons.EvaIcons
+import compose.icons.evaicons.Fill
+import compose.icons.evaicons.fill.ArrowLeft
+import compose.icons.evaicons.fill.ArrowRight
@Composable
fun ImagePreviewOverlay(
- imageState: State,
+ image: ImageBitmap,
onDismiss: () -> Unit,
+ onBack: () -> Unit,
+ onForward: () -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
@@ -40,12 +49,57 @@ fun ImagePreviewOverlay(
onClick = onDismiss,
),
) {
- imageState.onSuccess {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ // imageState.onSuccess {
Image(
- bitmap = it,
+ bitmap = image,
contentDescription = null,
- modifier = Modifier.fillMaxSize().padding(Padding.IMAGE.dp),
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(.9f)
+ .padding(Padding.IMAGE.dp),
)
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(Padding.STANDARD.dp)
+ .height(44.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Button(
+ onClick = onBack,
+ colors =
+ ButtonDefaults.buttonColors(
+ backgroundColor = FotoColors.primary.composeColor,
+ ),
+ ) {
+ Icon(
+ EvaIcons.Fill.ArrowLeft,
+ contentDescription = null,
+ tint = FotoColors.surface.composeColor,
+ )
+ }
+ Button(
+ onClick = onForward,
+ colors =
+ ButtonDefaults.buttonColors(
+ backgroundColor = FotoColors.primary.composeColor,
+ ),
+ ) {
+ Icon(
+ EvaIcons.Fill.ArrowRight,
+ contentDescription = null,
+ tint = FotoColors.surface.composeColor,
+ )
+ }
+ }
+ }
+ /*
}.onLoading {
CircularProgressIndicator(
modifier = Modifier.width(75.dp).align(Alignment.Center),
@@ -53,6 +107,6 @@ fun ImagePreviewOverlay(
)
}.onError {
Text("Error")
- }
+ }*/
}
}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/directory/DirectoryGrid.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/directory/DirectoryGrid.kt
index dad3dc10..fd0ad089 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/directory/DirectoryGrid.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/directory/DirectoryGrid.kt
@@ -13,12 +13,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
-import com.kevinschildhorn.fotopresenter.data.State
import com.kevinschildhorn.fotopresenter.ui.compose.common.ActionSheet
import com.kevinschildhorn.fotopresenter.ui.state.DirectoryGridState
import com.kevinschildhorn.fotopresenter.ui.state.FolderDirectoryGridCellState
@@ -31,7 +29,7 @@ fun DirectoryGrid(
gridSize: Int = 5,
modifier: Modifier = Modifier,
onFolderPressed: (Int) -> Unit,
- onImageDirectoryPressed: (State) -> Unit,
+ onImageDirectoryPressed: (Int) -> Unit,
) {
var actionSheetVisible by remember { mutableStateOf(false) }
var contextMenuPhotoId by rememberSaveable { mutableStateOf(null) }
@@ -48,7 +46,7 @@ fun DirectoryGrid(
.combinedClickable(
onClick = {
(state as? ImageDirectoryGridCellState)?.let { imageContent ->
- onImageDirectoryPressed(imageContent.imageState)
+ onImageDirectoryPressed(imageContent.id)
} ?: run {
onFolderPressed(state.id)
}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/login/LoginCheckbox.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/login/LoginCheckbox.kt
index f56f4d11..6ee1b297 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/login/LoginCheckbox.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/login/LoginCheckbox.kt
@@ -19,13 +19,13 @@ fun LoginCheckbox(
) {
Row(
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.End,
+ horizontalArrangement = horizontalArrangement,
modifier = modifier.fillMaxWidth(),
) {
Text(title)
Checkbox(
checked = checked,
- onCheckedChange = { onCheckedChange },
+ onCheckedChange = onCheckedChange,
)
}
}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/login/LoginPasswordTextField.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/login/LoginPasswordTextField.kt
index 66e8ca1a..6ead248e 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/login/LoginPasswordTextField.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/compose/login/LoginPasswordTextField.kt
@@ -1,13 +1,26 @@
package com.kevinschildhorn.fotopresenter.ui.compose.login
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
import com.kevinschildhorn.atomik.atomic.atoms.shape
import com.kevinschildhorn.atomik.atomic.atoms.textStyle
import com.kevinschildhorn.fotopresenter.ui.atoms.LoginScreenAtoms
+import compose.icons.EvaIcons
+import compose.icons.evaicons.Fill
+import compose.icons.evaicons.fill.Eye
+import compose.icons.evaicons.fill.EyeOff
@Composable
fun LoginPasswordTextField(
@@ -17,11 +30,12 @@ fun LoginPasswordTextField(
modifier: Modifier = Modifier,
) {
val molecule = LoginScreenAtoms.textFieldMolecule
+ var passwordVisible by rememberSaveable { mutableStateOf(false) }
OutlinedTextField(
value = value,
onValueChange = onValueChange,
- visualTransformation = PasswordVisualTransformation(),
+ visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
placeholder = {
Text(
placeholder,
@@ -32,5 +46,20 @@ fun LoginPasswordTextField(
shape = molecule.shape,
textStyle = molecule.textAtom.textStyle,
modifier = modifier,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
+ trailingIcon = {
+ val image =
+ if (passwordVisible) {
+ EvaIcons.Fill.Eye
+ } else {
+ EvaIcons.Fill.EyeOff
+ }
+
+ val description = if (passwordVisible) "Hide password" else "Show password"
+
+ IconButton(onClick = { passwordVisible = !passwordVisible }) {
+ Icon(imageVector = image, description)
+ }
+ },
)
}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt
new file mode 100644
index 00000000..c1c17df3
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt
@@ -0,0 +1,17 @@
+package com.kevinschildhorn.fotopresenter.ui.shared
+
+import androidx.compose.ui.graphics.ImageBitmap
+import io.github.reactivecircus.cache4k.Cache
+
+object SharedCache {
+ private val imageCache = Cache.Builder().build()
+
+ fun getImage(id: String): ImageBitmap? = imageCache.get(id)
+
+ fun cacheImage(
+ id: String,
+ imageBitmap: ImageBitmap,
+ ) {
+ imageCache.put(id, imageBitmap)
+ }
+}
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 a549381b..b5e38795 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
@@ -5,22 +5,3 @@ import androidx.compose.ui.graphics.ImageBitmap
expect class SharedImage {
fun getImageBitmap(size: Int): ImageBitmap?
}
-
-fun getScaledDimensions(
- width: Int,
- height: Int,
- minSize: Int,
-): Pair {
- val newWidth: Float
- val newHeight: Float
- if (height < width) {
- newHeight = minSize.toFloat()
- val ratio: Float = (newHeight / height)
- newWidth = width * ratio
- } else {
- newWidth = minSize.toFloat()
- val ratio: Float = (newWidth / width)
- newHeight = height * ratio
- }
- return Pair(newWidth.toInt(), newHeight.toInt())
-}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt
new file mode 100644
index 00000000..6eae73a1
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt
@@ -0,0 +1,28 @@
+package com.kevinschildhorn.fotopresenter.ui.shared
+
+import androidx.compose.ui.graphics.ImageBitmap
+
+expect object SharedImageConverter {
+ fun convertBytes(byteArray: ByteArray): ImageBitmap
+
+ fun convertImage(imageBitmap: ImageBitmap): ByteArray
+}
+
+fun getScaledDimensions(
+ width: Int,
+ height: Int,
+ minSize: Int,
+): Pair {
+ val newWidth: Float
+ val newHeight: Float
+ if (height < width) {
+ newHeight = minSize.toFloat()
+ val ratio: Float = (newHeight / height)
+ newWidth = width * ratio
+ } else {
+ newWidth = minSize.toFloat()
+ val ratio: Float = (newWidth / width)
+ newHeight = height * ratio
+ }
+ return Pair(newWidth.toInt(), newHeight.toInt())
+}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/state/DirectoryScreenState.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/state/DirectoryScreenState.kt
index 122cdabe..39d2a3cb 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/state/DirectoryScreenState.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/state/DirectoryScreenState.kt
@@ -2,11 +2,15 @@ package com.kevinschildhorn.fotopresenter.ui.state
import androidx.compose.ui.graphics.ImageBitmap
import com.kevinschildhorn.fotopresenter.data.State
+import com.kevinschildhorn.fotopresenter.extension.getNextIndex
+import com.kevinschildhorn.fotopresenter.extension.getPreviousIndex
data class DirectoryScreenState(
val currentPath: String = "",
var directoryGridState: DirectoryGridState = DirectoryGridState(emptyList(), mutableListOf()),
- var selectedImage: State = State.IDLE,
+ val selectedImageIndex: Int? = null,
+ val selectedImage: ImageBitmap? = null,
+ val loggedIn: Boolean = true,
override val state: UiState = UiState.IDLE,
) : ScreenState {
fun copyImageState(
@@ -29,6 +33,29 @@ data class DirectoryScreenState(
),
)
}
+
+ fun getImageIndexFromId(id: Int): Int = directoryGridState.imageStates.indexOfFirst { it.id == id }
+
+ fun getImageStateByIndex(): State? =
+ selectedImageIndex?.let { index ->
+ directoryGridState.imageStates.getOrNull(index)?.imageState
+ }
+
+ fun getNextImageIndex(): Int? {
+ selectedImageIndex?.let {
+ return directoryGridState.imageStates.getNextIndex(it)
+ } ?: run {
+ return null
+ }
+ }
+
+ fun getPreviousImageIndex(): Int? {
+ selectedImageIndex?.let {
+ return directoryGridState.imageStates.getPreviousIndex(it)
+ } ?: run {
+ return null
+ }
+ }
}
data class DirectoryGridState(
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/state/LoginScreenState.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/state/LoginScreenState.kt
index 053f2482..d3ab2ad5 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/state/LoginScreenState.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/state/LoginScreenState.kt
@@ -33,5 +33,6 @@ data class LoginScreenState(
username = username,
password = password,
sharedFolder = sharedFolder,
+ shouldAutoConnect = shouldAutoConnect,
)
}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/DirectoryViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/DirectoryViewModel.kt
index 2b139e1f..7d62e534 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/DirectoryViewModel.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/DirectoryViewModel.kt
@@ -6,7 +6,9 @@ import com.kevinschildhorn.fotopresenter.data.State
import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails
import com.kevinschildhorn.fotopresenter.data.network.NetworkHandlerException
import com.kevinschildhorn.fotopresenter.domain.ChangeDirectoryUseCase
+import com.kevinschildhorn.fotopresenter.domain.LogoutUseCase
import com.kevinschildhorn.fotopresenter.domain.RetrieveDirectoryContentsUseCase
+import com.kevinschildhorn.fotopresenter.domain.RetrieveImagesUseCase
import com.kevinschildhorn.fotopresenter.extension.addPath
import com.kevinschildhorn.fotopresenter.ui.state.DirectoryGridState
import com.kevinschildhorn.fotopresenter.ui.state.DirectoryScreenState
@@ -37,6 +39,57 @@ class DirectoryViewModel(
updateDirectories()
}
+ fun setLoggedIn() {
+ _uiState.update { it.copy(loggedIn = true) }
+ }
+
+ fun logout() {
+ viewModelScope.launch(Dispatchers.Default) {
+ val logoutUseCase: LogoutUseCase by inject()
+ logoutUseCase()
+ _uiState.update { it.copy(loggedIn = false) }
+ }
+ }
+ //region Image
+
+ fun showPreviousImage() {
+ val newIndex = _uiState.value.getPreviousImageIndex()
+ _uiState.update { it.copy(selectedImageIndex = newIndex) }
+ updateSelectedImage()
+ }
+
+ fun showNextImage() {
+ val newIndex = _uiState.value.getNextImageIndex()
+ _uiState.update { it.copy(selectedImageIndex = newIndex) }
+ updateSelectedImage()
+ }
+
+ fun clearPresentedImage() {
+ _uiState.update { it.copy(selectedImage = null, selectedImageIndex = null) }
+ }
+
+ fun setSelectedImageById(imageId: Int?) {
+ var index: Int? = null
+ imageId?.let {
+ index = _uiState.value.getImageIndexFromId(it)
+ }
+ _uiState.update { it.copy(selectedImageIndex = index) }
+ updateSelectedImage()
+ }
+
+ private fun updateSelectedImage() {
+ val state =
+ _uiState.value.getImageStateByIndex()?.let { state ->
+ _uiState.update { it.copy(selectedImage = state.value) }
+ } ?: run {
+ _uiState.update { it.copy(selectedImage = null) }
+ }
+ }
+
+ //endregion
+
+ //region Directory
+
fun changeDirectory(id: Int) {
_directoryContentsState.value.allDirectories.find { it.id == id }?.let {
changeDirectory(it.details)
@@ -99,19 +152,9 @@ class DirectoryViewModel(
private fun updatePhotos() {
_directoryContentsState.value.images.forEach { imageDirectory ->
viewModelScope.launch(Dispatchers.Default) {
- _uiState.update {
- it.copyImageState(
- imageDirectory.id,
- state = State.LOADING,
- )
- }
-
- val newState =
- imageDirectory.image?.getImageBitmap(400)?.let {
- State.SUCCESS(it)
- } ?: State.ERROR("No Image Found")
+ val retrieveImagesUseCase: RetrieveImagesUseCase by inject()
- viewModelScope.launch(Dispatchers.Main) {
+ retrieveImagesUseCase(imageDirectory) { newState ->
_uiState.update {
it.copyImageState(
imageDirectory.id,
@@ -136,4 +179,6 @@ class DirectoryViewModel(
)
}.toMutableList(),
)
+
+ //endregion
}
diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModel.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModel.kt
index 9782c1b4..63ca3147 100644
--- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModel.kt
+++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/viewmodel/LoginViewModel.kt
@@ -88,12 +88,16 @@ class LoginViewModel(
}
}
+ fun setLoggedOut() {
+ _uiState.update { it.copy(state = UiState.IDLE) }
+ }
+
private fun attemptAutoLogin() {
logger.i { "Attempting To Auto Login" }
- viewModelScope.launch {
+ viewModelScope.launch(Dispatchers.Default) {
val autoConnectUseCase: AutoConnectUseCase by inject()
if (autoConnectUseCase()) {
- // TODO
+ _uiState.update { it.copy(state = UiState.SUCCESS) }
}
}
}
diff --git a/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/domain/LogoutUseCaseTest.kt b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/domain/LogoutUseCaseTest.kt
new file mode 100644
index 00000000..6320ea3c
--- /dev/null
+++ b/shared/src/commonTest/kotlin/com/kevinschildhorn/fotopresenter/domain/LogoutUseCaseTest.kt
@@ -0,0 +1,49 @@
+package com.kevinschildhorn.fotopresenter.domain
+
+import com.kevinschildhorn.fotopresenter.data.network.MockNetworkHandler
+import com.kevinschildhorn.fotopresenter.testingModule
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.test.KoinTest
+import org.koin.test.inject
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.assertFalse
+
+/**
+Testing [LogoutUseCase]
+ **/
+class LogoutUseCaseTest : KoinTest {
+ private val useCase: LogoutUseCase by inject()
+
+ @BeforeTest
+ fun startTest() {
+ startKoin {
+ modules(testingModule())
+ }
+ }
+
+ @AfterTest
+ fun tearDown() {
+ stopKoin()
+ }
+
+ @Test
+ fun `logout Success`() =
+ runBlocking {
+ useCase()
+ assertFalse(MockNetworkHandler.isConnected, "Failed to Disconnect")
+ }
+
+ @Test
+ fun `logout Success Safe`() =
+ runBlocking {
+ MockNetworkHandler.disconnect()
+ assertFalse(MockNetworkHandler.isConnected, "Failed to Disconnect")
+ useCase()
+ assertFalse(MockNetworkHandler.isConnected, "Failed to Disconnect")
+ }
+
+}
\ No newline at end of file
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 f3b113a9..5999c779 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
@@ -30,7 +30,7 @@ class DirectoryViewModelTest : KoinTest {
fun tearDown() {
stopKoin()
}
-
+/*
@Test
fun `UI State`() =
runTest {
@@ -52,5 +52,5 @@ class DirectoryViewModelTest : KoinTest {
// assertEquals(2, directoryContents.allDirectories.count())
}
MockNetworkHandler.disconnect()
- }
+ }*/
}
diff --git a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt
index 57d2d206..feda2259 100644
--- a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt
+++ b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt
@@ -6,3 +6,4 @@ import com.hierynomus.smbj.share.File
actual fun getBitmapFromFile(file: File, size:Int): ImageBitmap? =
file.inputStream.buffered().use(::loadImageBitmap)
+
diff --git a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt
new file mode 100644
index 00000000..eac3ecad
--- /dev/null
+++ b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageConverter.kt
@@ -0,0 +1,18 @@
+package com.kevinschildhorn.fotopresenter.ui.shared
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asSkiaBitmap
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import org.jetbrains.skia.Image
+import kotlin.jvm.Throws
+
+actual object SharedImageConverter {
+ actual fun convertBytes(byteArray: ByteArray): ImageBitmap {
+ return Image.makeFromEncoded(byteArray).toComposeImageBitmap()
+ }
+
+ @Throws(Exception::class)
+ actual fun convertImage(imageBitmap: ImageBitmap): ByteArray {
+ return Image.makeFromBitmap(imageBitmap.asSkiaBitmap()).encodeToData()?.bytes!!
+ }
+}
\ No newline at end of file