From 7cd97e2ebbe38a07d6642819df1c1866533bfacb Mon Sep 17 00:00:00 2001 From: Adam Cedro Date: Mon, 12 May 2025 21:11:59 +0200 Subject: [PATCH 1/2] Add photos timestamp ordering --- .../backend/client/PrivMxClient.android.kt | 6 +- .../backend/client/IPrivMxClient.kt | 2 +- .../services/FileCabinetService.kt | 2 +- .../services/IFileCabinetService.kt | 2 +- .../main/filesCabinet/PhotosTabContent.kt | 82 ++++++++++++------- .../github/familyvault/utils/TimeFormatter.kt | 12 +++ 6 files changed, 71 insertions(+), 35 deletions(-) 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 50acf28a..d3210c08 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/kotlin/com/github/familyvault/backend/client/IPrivMxClient.kt b/composeApp/src/commonMain/kotlin/com/github/familyvault/backend/client/IPrivMxClient.kt index fed78eaf..ffce5481 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 8d869fa7..90f95d4c 100644 --- a/composeApp/src/commonMain/kotlin/com/github/familyvault/services/FileCabinetService.kt +++ b/composeApp/src/commonMain/kotlin/com/github/familyvault/services/FileCabinetService.kt @@ -77,7 +77,7 @@ class FileCabinetService( storeId: String?, limit: Long, skip: Long - ): List { + ): List> { return privMxClient.getFilesAsByteArrayFromStore(storeId, limit, skip) } 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 7de1be3c..18f2e4b3 100644 --- a/composeApp/src/commonMain/kotlin/com/github/familyvault/services/IFileCabinetService.kt +++ b/composeApp/src/commonMain/kotlin/com/github/familyvault/services/IFileCabinetService.kt @@ -11,6 +11,6 @@ interface IFileCabinetService { imageByteArray: ByteArray ) - fun getImagesFromFamilyGroupStoreAsByteArray(storeId: String?, limit: Long, skip: Long) : List + fun getImagesFromFamilyGroupStoreAsByteArray(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 055e8968..fc318329 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,17 +1,15 @@ package com.github.familyvault.ui.screens.main.filesCabinet import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.foundation.lazy.grid.items as gridItems +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import com.github.familyvault.services.IFileCabinetService import com.github.familyvault.services.IImagePickerService @@ -20,11 +18,15 @@ 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.loading import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject @@ -34,18 +36,18 @@ fun PhotosTabContent() { val imagePicker = koinInject() val storeId = fileCabinetService.retrieveFileCabinetStoreId() - var imageByteArrays by remember { mutableStateOf>(emptyList()) } + var imageData by remember { mutableStateOf>>(emptyList()) } var isLoading by remember { mutableStateOf(true) } var fullScreenImage by remember { mutableStateOf(null) } LaunchedEffect(Unit) { isLoading = true - imageByteArrays = withContext(Dispatchers.IO) { + imageData = withContext(Dispatchers.IO) { fileCabinetService.getImagesFromFamilyGroupStoreAsByteArray( storeId = storeId, limit = 30, skip = 0 - ).filterNotNull() + ) } isLoading = false } @@ -53,41 +55,61 @@ 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 { + val groupedByDate = remember(imageData) { + imageData + .sortedByDescending { it.second } + .groupBy { (_, timestamp) -> TimeFormatter.formatDate(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 { (date, images) -> + item(span = { GridItemSpan(maxLineSpan) }) { + Text( + text = date, + 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 } - ) + gridItems(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..d18118a0 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,9 @@ package com.github.familyvault.utils +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime object TimeFormatter { fun formatTime(time: LocalDateTime): String = @@ -15,4 +18,13 @@ object TimeFormatter { return minuteString } + + fun formatDate(timestamp: Long): String { + val fileDateTime = Instant.fromEpochMilliseconds(timestamp) + .toLocalDateTime(TimeZone.currentSystemDefault()) + val fileDate = fileDateTime.date + + val monthName = fileDate.month.name.lowercase().replaceFirstChar { it.uppercase() } + return "${fileDate.dayOfMonth} $monthName ${fileDate.year}" + } } \ No newline at end of file From 3c5fdc8a30a26893001e5fccf0f06d52ff0147d6 Mon Sep 17 00:00:00 2001 From: 6ajmon <6ajmon@gmail.com> Date: Wed, 21 May 2025 14:55:54 +0200 Subject: [PATCH 2/2] Add month name translating --- .../composeResources/values-en/strings.xml | 14 +++++++++ .../composeResources/values-pl/strings.xml | 14 +++++++++ .../main/filesCabinet/PhotosTabContent.kt | 31 ++++++++++++++----- .../github/familyvault/utils/TimeFormatter.kt | 8 ++--- 4 files changed, 56 insertions(+), 11 deletions(-) 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/ui/screens/main/filesCabinet/PhotosTabContent.kt b/composeApp/src/commonMain/kotlin/com/github/familyvault/ui/screens/main/filesCabinet/PhotosTabContent.kt index 33caca79..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 @@ -36,16 +36,32 @@ 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 import kotlinx.coroutines.withContext -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime 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() @@ -75,7 +91,6 @@ fun PhotosTabContent() { imageData.addAll(images) } fileCabinetListenerService.startListeningForNewFiles(storeId) { newImageBytes -> - // Add with current timestamp imageData.add(Pair(newImageBytes, System.currentTimeMillis())) } } catch (e: Exception) { @@ -127,7 +142,7 @@ fun PhotosTabContent() { val groupedByDate = remember(imageData) { imageData .sortedByDescending { it.second } - .groupBy { (_, timestamp) -> TimeFormatter.formatDate(timestamp) } + .groupBy { (_, timestamp) -> TimeFormatter.formatDateParts(timestamp) } } LazyVerticalGrid( @@ -137,10 +152,12 @@ fun PhotosTabContent() { horizontalArrangement = Arrangement.spacedBy(AdditionalTheme.spacings.small), contentPadding = PaddingValues(AdditionalTheme.spacings.small) ) { - groupedByDate.forEach { (date, images) -> + groupedByDate.forEach { (dateParts, images) -> item(span = { GridItemSpan(maxLineSpan) }) { + val monthName = stringResource(monthStringResources[dateParts.month - 1]) + val dateText = "${dateParts.day} $monthName ${dateParts.year}" Text( - text = date, + text = dateText, style = MaterialTheme.typography.titleMedium, modifier = Modifier .fillMaxWidth() 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 d18118a0..3243be22 100644 --- a/composeApp/src/commonMain/kotlin/com/github/familyvault/utils/TimeFormatter.kt +++ b/composeApp/src/commonMain/kotlin/com/github/familyvault/utils/TimeFormatter.kt @@ -5,6 +5,8 @@ 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 = "${time.hour}:${addZeroToMinuteIfNeeded(time.minute)}" @@ -19,12 +21,10 @@ object TimeFormatter { return minuteString } - fun formatDate(timestamp: Long): String { + fun formatDateParts(timestamp: Long): DateParts { val fileDateTime = Instant.fromEpochMilliseconds(timestamp) .toLocalDateTime(TimeZone.currentSystemDefault()) val fileDate = fileDateTime.date - - val monthName = fileDate.month.name.lowercase().replaceFirstChar { it.uppercase() } - return "${fileDate.dayOfMonth} $monthName ${fileDate.year}" + return DateParts(day = fileDate.dayOfMonth, month = fileDate.month.value, year = fileDate.year) } } \ No newline at end of file