Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ class PrivMxClient : IPrivMxClient, AutoCloseable {
return data
}

override fun getFilesAsByteArrayFromStore(storeId: String?, limit: Long, skip: Long): List<ByteArray> {
override fun getFilesAsByteArrayFromStore(storeId: String?, limit: Long, skip: Long): List<Pair<ByteArray, Long>> {
val files = storeApi!!.listFiles(
storeId,
skip,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ interface IPrivMxClient {
)

fun getFileAsByteArrayFromStore(fileId: String): ByteArray
fun getFilesAsByteArrayFromStore(storeId: String?, limit: Long, skip: Long): List<ByteArray>
fun getFilesAsByteArrayFromStore(storeId: String?, limit: Long, skip: Long): List<Pair<ByteArray, Long>>
fun sendByteArrayToStore(storeId: String, content: ByteArray): String
fun retrieveMessagesFromThread(
threadId: String, startIndex: Int, pageSize: Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class FileCabinetService(
storeId: String?,
limit: Long,
skip: Long
): List<ByteArray> {
): List<Pair<ByteArray, Long>> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dla bezpieczeństwa warto wydzielić to do innej metody a ten powinien zostać tak jak był w PrivMxClient, też zamiast Pair<...,...> powinien być jakiś model.

val actualStoreId = storeId ?: retrieveFileCabinetImagesStoreId()
return privMxClient.getFilesAsByteArrayFromStore(actualStoreId, limit, skip)
}
Expand Down Expand Up @@ -193,7 +193,7 @@ class FileCabinetService(
storeId: String?,
limit: Long,
skip: Long
): List<ByteArray> {
): List<Pair<ByteArray, Long>> {
val actualStoreId = storeId ?: retrieveFileCabinetDocumentsStoreId()
return privMxClient.getFilesAsByteArrayFromStore(actualStoreId, limit, skip)
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
)
}
}
Expand All @@ -255,15 +255,15 @@ class FileCabinetService(
content = byteArray,
fileName = fileName,
mimeType = mimeType,
uploadDate = System.currentTimeMillis() - (index * 1000)
uploadDate = timestamp
)
} catch (e: Exception) {
val isPdf = FileTypeUtils.isPdfFile(byteArray)
DocumentWithMetadata(
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
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ interface IFileCabinetService {
documentMimeType: String
)

fun getImagesFromFamilyGroupStoreAsByteArray(storeId: String?, limit: Long, skip: Long): List<ByteArray>
fun getDocumentsFromFamilyGroupStore(storeId: String?, limit: Long, skip: Long): List<ByteArray>
fun getDocumentsFromFamilyGroupStore(storeId: String?, limit: Long, skip: Long): List<Pair<ByteArray, Long>>

fun getImagesFromFamilyGroupStoreAsByteArray(storeId: String?, limit: Long, skip: Long) : List<Pair<ByteArray, Long>>
fun getDocumentsWithMetadataFromStore(storeId: String?, limit: Long, skip: Long): List<DocumentWithMetadata>

suspend fun restoreFileCabinetMembership()
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -28,6 +31,7 @@ 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
Expand All @@ -36,6 +40,9 @@ 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

Expand All @@ -46,7 +53,7 @@ fun PhotosTabContent() {
val imagePicker = koinInject<IImagePickerService>()
val coroutineScope = rememberCoroutineScope()

val imageByteArrays = remember { mutableStateListOf<ByteArray>() }
val imageData = remember { mutableStateListOf<Pair<ByteArray, Long>>() }
var isLoading by remember { mutableStateOf(true) }
var fullScreenImage by remember { mutableStateOf<ImageBitmap?>(null) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Expand All @@ -58,20 +65,22 @@ 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 ->
// Add with current timestamp
imageData.add(Pair(newImageBytes, System.currentTimeMillis()))
}
} catch (e: Exception) {
errorMessage = "Error loading images: ${e.message}"
imageByteArrays.clear()
imageData.clear()
} finally {
isLoading = false
}
Expand All @@ -90,7 +99,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) {
Expand All @@ -109,42 +119,61 @@ 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.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<ImageBitmap?>(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 }
)
items(images) { (imageBytes, _) ->
val imageBitmapState =
produceState<ImageBitmap?>(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 }
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -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

Comment thread
6ajmon marked this conversation as resolved.
Outdated
val monthName = fileDate.month.name.lowercase().replaceFirstChar { it.uppercase() }
return "${fileDate.dayOfMonth} $monthName ${fileDate.year}"
}
}