Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions composeApp/src/commonMain/composeResources/values-en/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,18 @@
<string name="file_cabinet_initializing_documents">Initializing documents storage...</string>
<string name="file_cabinet_error_initialize">Could not initialize documents storage</string>
<string name="file_cabinet_error_loading">Error loading documents</string>

<!-- Months -->
<string name="month_january">January</string>
<string name="month_february">February</string>
<string name="month_march">March</string>
<string name="month_april">April</string>
<string name="month_may">May</string>
<string name="month_june">June</string>
<string name="month_july">July</string>
<string name="month_august">August</string>
<string name="month_september">September</string>
<string name="month_october">October</string>
<string name="month_november">November</string>
<string name="month_december">December</string>
</resources>
14 changes: 14 additions & 0 deletions composeApp/src/commonMain/composeResources/values-pl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,18 @@
<string name="file_cabinet_initializing_documents">Inicjalizacja przechowywania dokumentów...</string>
<string name="file_cabinet_error_initialize">Nie można zainicjować przechowywania dokumentów</string>
<string name="file_cabinet_error_loading">Błąd podczas ładowania dokumentów</string>

<!-- Months -->
<string name="month_january">Styczeń</string>
<string name="month_february">Luty</string>
<string name="month_march">Marzec</string>
<string name="month_april">Kwiecień</string>
<string name="month_may">Maj</string>
<string name="month_june">Czerwiec</string>
<string name="month_july">Lipiec</string>
<string name="month_august">Sierpień</string>
<string name="month_september">Wrzesień</string>
<string name="month_october">Październik</string>
<string name="month_november">Listopad</string>
<string name="month_december">Grudzień</string>
</resources>
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,25 +31,45 @@ 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
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<IFileCabinetService>()
val fileCabinetListenerService = koinInject<IFileCabinetListenerService>()
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 +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
}
Expand All @@ -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) {
Expand All @@ -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<ImageBitmap?>(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<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,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 =
Expand All @@ -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)
}
}