From c215ae3ec48622adcfcf0adc3dddbbec63f383f9 Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Mon, 23 Feb 2026 18:29:23 +0100 Subject: [PATCH] feat: Added UI for the FolderPairTab This commit adds a FolderPairCard composable. Also fixed the warning: "Serializable object must implement 'readResolve'". --- .../cloudsync/ui/screens/HomeScreen.kt | 2 + .../fabisahne/cloudsync/ui/tabs/AccountTab.kt | 2 + .../cloudsync/ui/tabs/FolderPairTab.kt | 165 ++++++++++++++---- .../fabisahne/cloudsync/ui/tabs/HistoryTab.kt | 2 + .../cloudsync/ui/tabs/SettingsTab.kt | 2 + 5 files changed, 142 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/fabisahne/cloudsync/ui/screens/HomeScreen.kt b/app/src/main/java/com/fabisahne/cloudsync/ui/screens/HomeScreen.kt index ef06f3e..1dc2888 100644 --- a/app/src/main/java/com/fabisahne/cloudsync/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/fabisahne/cloudsync/ui/screens/HomeScreen.kt @@ -23,6 +23,8 @@ import com.fabisahne.cloudsync.ui.tabs.SettingsTab object HomeScreen : Screen { + private fun readResolve(): Any = HomeScreen + private val TABS = listOf( HistoryTab, FolderPairTab, diff --git a/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/AccountTab.kt b/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/AccountTab.kt index 17b0aa7..e1926b0 100644 --- a/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/AccountTab.kt +++ b/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/AccountTab.kt @@ -11,6 +11,8 @@ import cafe.adriel.voyager.navigator.tab.TabOptions object AccountTab : Tab { + private fun readResolve(): Any = AccountTab + override val options: TabOptions @Composable get() { diff --git a/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/FolderPairTab.kt b/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/FolderPairTab.kt index 1e7c97b..bf80acf 100644 --- a/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/FolderPairTab.kt +++ b/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/FolderPairTab.kt @@ -4,14 +4,23 @@ import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +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.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.FolderCopy +import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,17 +28,30 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import com.fabisahne.cloudsync.data.AppDatabase import com.fabisahne.cloudsync.data.FolderPair +import com.fabisahne.cloudsync.ui.screens.FolderPairScreen +import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme import kotlinx.coroutines.launch -import androidx.core.net.toUri +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter object FolderPairTab : Tab { + private fun readResolve(): Any = FolderPairTab + override val options: TabOptions @Composable get() { @@ -47,38 +69,37 @@ object FolderPairTab : Tab { @Composable override fun Content() { -// Text("Folder Pairs") -// FloatingActionButton( -// -// modifier = Modifier.absolute(), -// onClick = {} -// ) { -// Icon(Icons.Default.Add, "Add Pair") -// } + val navigator = LocalNavigator.currentOrThrow.parent!! + val context = LocalContext.current val scope = rememberCoroutineScope() val dao = remember { AppDatabase.getDatabase(context).folderPairDao() } val folderPairs by dao.getAll().collectAsState(initial = emptyList()) - val directoryPickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocumentTree() - ) { uri: Uri? -> - uri?.let { - val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - context.contentResolver.takePersistableUriPermission(it, takeFlags) + val directoryPickerLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? -> + uri?.let { + val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + context.contentResolver.takePersistableUriPermission(it, takeFlags) - // Save to Room - scope.launch { - dao.insert( - FolderPair( - name = "New Folder Pair", - localDir = it.toString(), - cloudPath = "/backup" + // Save to Room + scope.launch { + dao.insert( + FolderPair( + name = "New Folder Pair", + localDir = it.toString(), + cloudPath = "/backup" + ) ) - ) + } } } + + val deletePair = { pair: FolderPair -> + scope.launch { + dao.delete(pair) + } } Scaffold( @@ -95,16 +116,98 @@ object FolderPairTab : Tab { LazyColumn(modifier = Modifier.padding(paddingValues)) { items(folderPairs.size) { pairIdx -> val pair = folderPairs[pairIdx] - ListItem( - headlineContent = { Text(pair.name) }, - supportingContent = { - Text( - pair.localDir.toUri().lastPathSegment ?: pair.localDir - ) + FolderPairCard( + pair, + onDelete = { + deletePair(pair) + }, + onClick = { + navigator.push(FolderPairScreen(pair.id)) } ) } } } } +} + + +@Composable +fun FolderPairCard(folderPair: FolderPair, onDelete: () -> Unit = {}, onClick: () -> Unit = {}) { + val instant = Instant.ofEpochMilli(folderPair.createdAt) + val dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()) + + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + + Card( + onClick = onClick, + modifier = Modifier + .padding(8.dp, 8.dp) + .height(100.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(10.dp, 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Text(dateTime.format(formatter), style = MaterialTheme.typography.labelMedium) + Text(folderPair.name, style = MaterialTheme.typography.titleMedium) + Text( + folderPair.localDir.toUri().lastPathSegment ?: folderPair.localDir, + style = MaterialTheme.typography.labelMedium + ) + } + Button( + onClick = onDelete + ) { + Icon( + Icons.Default.Delete, + null, + ) + } + } + } +} + +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) +@Composable +fun FolderPairsCardPreview() { + val folderPair = FolderPair( + name = "Sample Folder Pair", + localDir = "sample/local/folder", + cloudPath = "sample/cloud/path" + ) + + CloudSyncTheme { + Column { + FolderPairCard(folderPair) + FolderPairCard(folderPair) + } + } +} + +@Preview(backgroundColor = 0xFF000000, showBackground = true) +@Composable +fun FolderPairsCardPreviewDark() { + val folderPair = FolderPair( + name = "Sample Folder Pair", + localDir = "sample/local/folder", + cloudPath = "sample/cloud/path" + ) + + CloudSyncTheme( + darkTheme = true + ) { + Column { + FolderPairCard(folderPair) + FolderPairCard(folderPair) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/HistoryTab.kt b/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/HistoryTab.kt index bee315d..90a94c7 100644 --- a/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/HistoryTab.kt +++ b/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/HistoryTab.kt @@ -32,6 +32,8 @@ import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme import java.time.LocalDate object HistoryTab : Tab { + private fun readResolve(): Any = HistoryTab + override val options: TabOptions @Composable get() { diff --git a/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/SettingsTab.kt b/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/SettingsTab.kt index f4c7e06..c009356 100644 --- a/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/SettingsTab.kt +++ b/app/src/main/java/com/fabisahne/cloudsync/ui/tabs/SettingsTab.kt @@ -11,6 +11,8 @@ import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions object SettingsTab : Tab { + private fun readResolve(): Any = SettingsTab + override val options: TabOptions @Composable get() {