diff --git a/composeApp/src/androidMain/kotlin/com/github/familyvault/backend/client/PrivMxClient.android.kt b/composeApp/src/androidMain/kotlin/com/github/familyvault/backend/client/PrivMxClient.android.kt index 88ad94a7..d7e6fc4a 100644 --- a/composeApp/src/androidMain/kotlin/com/github/familyvault/backend/client/PrivMxClient.android.kt +++ b/composeApp/src/androidMain/kotlin/com/github/familyvault/backend/client/PrivMxClient.android.kt @@ -290,7 +290,7 @@ class PrivMxClient : IPrivMxClient, AutoCloseable { return data } - override fun getFilesAsByteArrayFromStore(storeId: String?, limit: Long, skip: Long): List { + override fun getFilesAsByteArrayFromStore(storeId: String?, limit: Long, skip: Long): List> { val files = storeApi!!.listFiles( storeId, skip, @@ -300,7 +300,9 @@ class PrivMxClient : IPrivMxClient, AutoCloseable { return files.readItems.mapNotNull { file -> try { - getFileAsByteArrayFromStore(file.info.fileId) + val byteArray = getFileAsByteArrayFromStore(file.info.fileId) + val creationTimestamp = file.info.createDate + Pair(byteArray, creationTimestamp) } catch (e: Exception) { e.printStackTrace() null diff --git a/composeApp/src/commonMain/composeResources/values-en/strings.xml b/composeApp/src/commonMain/composeResources/values-en/strings.xml index bd70d8c1..ebcace05 100644 --- a/composeApp/src/commonMain/composeResources/values-en/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-en/strings.xml @@ -208,4 +208,18 @@ Initializing documents storage... Could not initialize documents storage Error loading documents + + + January + February + March + April + May + June + July + August + September + October + November + December \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml index d90781e0..c48ca8f3 100644 --- a/composeApp/src/commonMain/composeResources/values-pl/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml @@ -208,4 +208,18 @@ Inicjalizacja przechowywania dokumentów... Nie można zainicjować przechowywania dokumentów Błąd podczas ładowania dokumentów + + + Styczeń + Luty + Marzec + Kwiecień + Maj + Czerwiec + Lipiec + Sierpień + Wrzesień + Październik + Listopad + Grudzień \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/github/familyvault/backend/client/IPrivMxClient.kt b/composeApp/src/commonMain/kotlin/com/github/familyvault/backend/client/IPrivMxClient.kt index 703c70e3..4fa099a6 100644 --- a/composeApp/src/commonMain/kotlin/com/github/familyvault/backend/client/IPrivMxClient.kt +++ b/composeApp/src/commonMain/kotlin/com/github/familyvault/backend/client/IPrivMxClient.kt @@ -78,7 +78,7 @@ interface IPrivMxClient { ) fun getFileAsByteArrayFromStore(fileId: String): ByteArray - fun getFilesAsByteArrayFromStore(storeId: String?, limit: Long, skip: Long): List + fun getFilesAsByteArrayFromStore(storeId: String?, limit: Long, skip: Long): List> fun sendByteArrayToStore(storeId: String, content: ByteArray): String fun retrieveMessagesFromThread( threadId: String, startIndex: Int, pageSize: Int diff --git a/composeApp/src/commonMain/kotlin/com/github/familyvault/services/FileCabinetService.kt b/composeApp/src/commonMain/kotlin/com/github/familyvault/services/FileCabinetService.kt index b029425c..47d46ef8 100644 --- a/composeApp/src/commonMain/kotlin/com/github/familyvault/services/FileCabinetService.kt +++ b/composeApp/src/commonMain/kotlin/com/github/familyvault/services/FileCabinetService.kt @@ -152,7 +152,7 @@ class FileCabinetService( storeId: String?, limit: Long, skip: Long - ): List { + ): List> { val actualStoreId = storeId ?: retrieveFileCabinetImagesStoreId() return privMxClient.getFilesAsByteArrayFromStore(actualStoreId, limit, skip) } @@ -193,7 +193,7 @@ class FileCabinetService( storeId: String?, limit: Long, skip: Long - ): List { + ): List> { val actualStoreId = storeId ?: retrieveFileCabinetDocumentsStoreId() return privMxClient.getFilesAsByteArrayFromStore(actualStoreId, limit, skip) } @@ -206,7 +206,7 @@ class FileCabinetService( val actualStoreId = storeId ?: retrieveFileCabinetDocumentsStoreId() val rawBytes = privMxClient.getFilesAsByteArrayFromStore(actualStoreId, limit, skip) - return rawBytes.mapIndexed { index, byteArray -> + return rawBytes.mapIndexed { index, (byteArray, timestamp) -> try { if (byteArray.size >= 4) { val metadataLength = (byteArray[0].toInt() and 0xFF shl 24) or @@ -225,7 +225,7 @@ class FileCabinetService( content = contentBytes, fileName = metadata.name, mimeType = metadata.mime, - uploadDate = metadata.timestamp + uploadDate = metadata.timestamp ?: timestamp ) } catch (e: Exception) { // Fallback to the previous regex approach if JSON parsing fails @@ -235,13 +235,13 @@ class FileCabinetService( val fileName = nameMatch?.groupValues?.get(1) val mimeType = mimeMatch?.groupValues?.get(1) - val timestamp = timestampMatch?.groupValues?.get(1)?.toLongOrNull() + val parsedTimestamp = timestampMatch?.groupValues?.get(1)?.toLongOrNull() DocumentWithMetadata( content = contentBytes, fileName = fileName, mimeType = mimeType, - uploadDate = timestamp + uploadDate = parsedTimestamp ?: timestamp ) } } @@ -255,7 +255,7 @@ class FileCabinetService( content = byteArray, fileName = fileName, mimeType = mimeType, - uploadDate = System.currentTimeMillis() - (index * 1000) + uploadDate = timestamp ) } catch (e: Exception) { val isPdf = FileTypeUtils.isPdfFile(byteArray) @@ -263,7 +263,7 @@ class FileCabinetService( content = byteArray, fileName = if (isPdf) "Document_${index}.pdf" else "File_${index}.jpg", mimeType = if (isPdf) "application/pdf" else "image/jpeg", - uploadDate = System.currentTimeMillis() - (index * 1000) + uploadDate = timestamp ) } } diff --git a/composeApp/src/commonMain/kotlin/com/github/familyvault/services/IFileCabinetService.kt b/composeApp/src/commonMain/kotlin/com/github/familyvault/services/IFileCabinetService.kt index c00a6fa2..9a7869ca 100644 --- a/composeApp/src/commonMain/kotlin/com/github/familyvault/services/IFileCabinetService.kt +++ b/composeApp/src/commonMain/kotlin/com/github/familyvault/services/IFileCabinetService.kt @@ -21,10 +21,10 @@ interface IFileCabinetService { documentMimeType: String ) - fun getImagesFromFamilyGroupStoreAsByteArray(storeId: String?, limit: Long, skip: Long): List - fun getDocumentsFromFamilyGroupStore(storeId: String?, limit: Long, skip: Long): List + fun getDocumentsFromFamilyGroupStore(storeId: String?, limit: Long, skip: Long): List> + fun getImagesFromFamilyGroupStoreAsByteArray(storeId: String?, limit: Long, skip: Long) : List> fun getDocumentsWithMetadataFromStore(storeId: String?, limit: Long, skip: Long): List - + suspend fun restoreFileCabinetMembership() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/github/familyvault/ui/screens/main/filesCabinet/PhotosTabContent.kt b/composeApp/src/commonMain/kotlin/com/github/familyvault/ui/screens/main/filesCabinet/PhotosTabContent.kt index 1ec069f1..73a58e13 100644 --- a/composeApp/src/commonMain/kotlin/com/github/familyvault/ui/screens/main/filesCabinet/PhotosTabContent.kt +++ b/composeApp/src/commonMain/kotlin/com/github/familyvault/ui/screens/main/filesCabinet/PhotosTabContent.kt @@ -1,12 +1,10 @@ package com.github.familyvault.ui.screens.main.filesCabinet import androidx.compose.foundation.layout.* +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material3.Button import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -16,7 +14,12 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.foundation.layout.Arrangement import androidx.compose.ui.unit.dp @@ -28,10 +31,23 @@ import com.github.familyvault.ui.components.LoaderWithText import com.github.familyvault.ui.components.filesCabinet.LoadingCard import com.github.familyvault.ui.components.filesCabinet.PhotoCard import com.github.familyvault.ui.theme.AdditionalTheme +import com.github.familyvault.utils.TimeFormatter import familyvault.composeapp.generated.resources.Res import familyvault.composeapp.generated.resources.file_cabinet_no_images import familyvault.composeapp.generated.resources.file_cabinet_retry import familyvault.composeapp.generated.resources.loading +import familyvault.composeapp.generated.resources.month_april +import familyvault.composeapp.generated.resources.month_august +import familyvault.composeapp.generated.resources.month_december +import familyvault.composeapp.generated.resources.month_february +import familyvault.composeapp.generated.resources.month_january +import familyvault.composeapp.generated.resources.month_july +import familyvault.composeapp.generated.resources.month_june +import familyvault.composeapp.generated.resources.month_march +import familyvault.composeapp.generated.resources.month_may +import familyvault.composeapp.generated.resources.month_november +import familyvault.composeapp.generated.resources.month_october +import familyvault.composeapp.generated.resources.month_september import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.launch @@ -39,6 +55,13 @@ import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject +private val monthStringResources = arrayOf( + Res.string.month_january, Res.string.month_february, Res.string.month_march, + Res.string.month_april, Res.string.month_may, Res.string.month_june, + Res.string.month_july, Res.string.month_august, Res.string.month_september, + Res.string.month_october, Res.string.month_november, Res.string.month_december +) + @Composable fun PhotosTabContent() { val fileCabinetService = koinInject() @@ -46,7 +69,7 @@ fun PhotosTabContent() { val imagePicker = koinInject() val coroutineScope = rememberCoroutineScope() - val imageByteArrays = remember { mutableStateListOf() } + val imageData = remember { mutableStateListOf>() } var isLoading by remember { mutableStateOf(true) } var fullScreenImage by remember { mutableStateOf(null) } var errorMessage by remember { mutableStateOf(null) } @@ -58,20 +81,21 @@ fun PhotosTabContent() { try { val storeId = fileCabinetService.retrieveFileCabinetImagesStoreId() - imageByteArrays.clear() + imageData.clear() withContext(Dispatchers.IO) { - imageByteArrays.addAll(fileCabinetService.getImagesFromFamilyGroupStoreAsByteArray( + val images = fileCabinetService.getImagesFromFamilyGroupStoreAsByteArray( storeId = storeId, limit = 30, skip = 0 - )) + ) + imageData.addAll(images) } - fileCabinetListenerService.startListeningForNewFiles(storeId) { - imageByteArrays.add(it) + fileCabinetListenerService.startListeningForNewFiles(storeId) { newImageBytes -> + imageData.add(Pair(newImageBytes, System.currentTimeMillis())) } } catch (e: Exception) { errorMessage = "Error loading images: ${e.message}" - imageByteArrays.clear() + imageData.clear() } finally { isLoading = false } @@ -90,7 +114,8 @@ fun PhotosTabContent() { if (isLoading) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { LoaderWithText( - stringResource(Res.string.loading), modifier = Modifier.fillMaxSize() + stringResource(Res.string.loading), + modifier = Modifier.fillMaxSize() ) } } else if (errorMessage != null) { @@ -109,42 +134,63 @@ fun PhotosTabContent() { } } } - } else if (imageByteArrays.isEmpty()) { + } else if (imageData.isEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(Res.string.file_cabinet_no_images)) } } else { + val groupedByDate = remember(imageData) { + imageData + .sortedByDescending { it.second } + .groupBy { (_, timestamp) -> TimeFormatter.formatDateParts(timestamp) } + } + LazyVerticalGrid( columns = GridCells.Fixed(3), modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(AdditionalTheme.spacings.small), + horizontalArrangement = Arrangement.spacedBy(AdditionalTheme.spacings.small), contentPadding = PaddingValues(AdditionalTheme.spacings.small) ) { - items(imageByteArrays.size) { index -> - val imageBytes = imageByteArrays[index] - - val imageBitmapState = produceState(initialValue = null, imageBytes) { - withContext(Dispatchers.IO) { - value = imagePicker.getBitmapFromBytes(imageBytes) - } + groupedByDate.forEach { (dateParts, images) -> + item(span = { GridItemSpan(maxLineSpan) }) { + val monthName = stringResource(monthStringResources[dateParts.month - 1]) + val dateText = "${dateParts.day} $monthName ${dateParts.year}" + Text( + text = dateText, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .fillMaxWidth() + .padding(AdditionalTheme.spacings.medium) + ) } - val bitmap = imageBitmapState.value - if (bitmap == null) { - LoadingCard() - } else { - PhotoCard( - imageBitmap = bitmap, - onClick = { fullScreenImage = bitmap } - ) + items(images) { (imageBytes, _) -> + val imageBitmapState = + produceState(initialValue = null, imageBytes) { + withContext(Dispatchers.IO) { + value = imagePicker.getBitmapFromBytes(imageBytes) + } + } + + val bitmap = imageBitmapState.value + if (bitmap == null) { + LoadingCard() + } else { + PhotoCard( + imageBitmap = bitmap, + onClick = { fullScreenImage = bitmap } + ) + } } } } } - if (fullScreenImage != null) { + fullScreenImage?.let { image -> FullScreenImage( - imageBitmap = fullScreenImage!!, + imageBitmap = image, onDismiss = { fullScreenImage = null } ) } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/github/familyvault/utils/TimeFormatter.kt b/composeApp/src/commonMain/kotlin/com/github/familyvault/utils/TimeFormatter.kt index 215c5366..3243be22 100644 --- a/composeApp/src/commonMain/kotlin/com/github/familyvault/utils/TimeFormatter.kt +++ b/composeApp/src/commonMain/kotlin/com/github/familyvault/utils/TimeFormatter.kt @@ -1,6 +1,11 @@ package com.github.familyvault.utils +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +data class DateParts(val day: Int, val month: Int, val year: Int) object TimeFormatter { fun formatTime(time: LocalDateTime): String = @@ -15,4 +20,11 @@ object TimeFormatter { return minuteString } + + fun formatDateParts(timestamp: Long): DateParts { + val fileDateTime = Instant.fromEpochMilliseconds(timestamp) + .toLocalDateTime(TimeZone.currentSystemDefault()) + val fileDate = fileDateTime.date + return DateParts(day = fileDate.dayOfMonth, month = fileDate.month.value, year = fileDate.year) + } } \ No newline at end of file