Merge pull request 'Add Database | Add FolderPairDao | Add UI for FolderPairs' (#1) from ui into develop

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-02-23 18:38:25 +01:00
10 changed files with 258 additions and 14 deletions

View File

@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.jetbrains.kotlin.serialization) alias(libs.plugins.jetbrains.kotlin.serialization)
alias(libs.plugins.ksp)
} }
android { android {
@@ -63,4 +64,9 @@ dependencies {
implementation(libs.kotlinx.serialization.core) implementation(libs.kotlinx.serialization.core)
implementation(libs.bundles.voyager) implementation(libs.bundles.voyager)
// Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
} }

View File

@@ -0,0 +1,27 @@
package com.fabisahne.cloudsync.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [FolderPair::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun folderPairDao(): FolderPairDao
companion object {
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"cloud_sync_database",
).build()
INSTANCE = instance
instance
}
}
}
}

View File

@@ -0,0 +1,14 @@
package com.fabisahne.cloudsync.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "folder_pairs")
data class FolderPair(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val localDir: String,
val cloudPath: String,
val createdAt: Long = System.currentTimeMillis()
)

View File

@@ -0,0 +1,26 @@
package com.fabisahne.cloudsync.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface FolderPairDao {
@Query("SELECT * FROM folder_pairs ORDER BY createdAt DESC")
fun getAll(): Flow<List<FolderPair>>
@Query("SELECT * FROM folder_pairs WHERE id = :id")
suspend fun getById(id: Long): FolderPair?
@Insert
suspend fun insert(folderPair: FolderPair): Long
@Update
suspend fun update(folderPair: FolderPair)
@Delete
suspend fun delete(folderPair: FolderPair)
}

View File

@@ -23,6 +23,8 @@ import com.fabisahne.cloudsync.ui.tabs.SettingsTab
object HomeScreen : Screen { object HomeScreen : Screen {
private fun readResolve(): Any = HomeScreen
private val TABS = listOf( private val TABS = listOf(
HistoryTab, HistoryTab,
FolderPairTab, FolderPairTab,

View File

@@ -11,6 +11,8 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
object AccountTab : Tab { object AccountTab : Tab {
private fun readResolve(): Any = AccountTab
override val options: TabOptions override val options: TabOptions
@Composable @Composable
get() { get() {

View File

@@ -1,23 +1,57 @@
package com.fabisahne.cloudsync.ui.tabs package com.fabisahne.cloudsync.ui.tabs
import androidx.compose.foundation.layout.absoluteOffset import android.content.Intent
import androidx.compose.foundation.layout.absolutePadding 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.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FolderCopy 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.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter 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.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions 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 java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
object FolderPairTab : Tab { object FolderPairTab : Tab {
private fun readResolve(): Any = FolderPairTab
override val options: TabOptions override val options: TabOptions
@Composable @Composable
get() { get() {
@@ -35,25 +69,145 @@ object FolderPairTab : Tab {
@Composable @Composable
override fun Content() { override fun Content() {
// Text("Folder Pairs") val navigator = LocalNavigator.currentOrThrow.parent!!
// FloatingActionButton(
// val context = LocalContext.current
// modifier = Modifier.absolute(), val scope = rememberCoroutineScope()
// onClick = {} val dao = remember { AppDatabase.getDatabase(context).folderPairDao() }
// ) { val folderPairs by dao.getAll().collectAsState(initial = emptyList())
// Icon(Icons.Default.Add, "Add Pair")
// } 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"
)
)
}
}
}
val deletePair = { pair: FolderPair ->
scope.launch {
dao.delete(pair)
}
}
Scaffold( Scaffold(
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
onClick = {} onClick = {
directoryPickerLauncher.launch(null)
}
) { ) {
Icon(Icons.Default.Add, "Add Pair") Icon(Icons.Default.Add, "Add Pair")
} }
} }
) { paddingValues -> ) { paddingValues ->
Text("Pairs here", Modifier.padding(paddingValues)) LazyColumn(modifier = Modifier.padding(paddingValues)) {
items(folderPairs.size) { pairIdx ->
val pair = folderPairs[pairIdx]
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)
} }
} }
} }

View File

@@ -32,6 +32,8 @@ import com.fabisahne.cloudsync.ui.theme.CloudSyncTheme
import java.time.LocalDate import java.time.LocalDate
object HistoryTab : Tab { object HistoryTab : Tab {
private fun readResolve(): Any = HistoryTab
override val options: TabOptions override val options: TabOptions
@Composable @Composable
get() { get() {

View File

@@ -11,6 +11,8 @@ import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
object SettingsTab : Tab { object SettingsTab : Tab {
private fun readResolve(): Any = SettingsTab
override val options: TabOptions override val options: TabOptions
@Composable @Composable
get() { get() {

View File

@@ -1,14 +1,16 @@
[versions] [versions]
agp = "9.0.0" agp = "9.0.1"
coreKtx = "1.17.0" coreKtx = "1.17.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.2" activityCompose = "1.12.2"
kotlin = "2.0.21" kotlin = "2.2.10"
composeBom = "2024.09.00" composeBom = "2024.09.00"
voyager = "1.1.0-beta03" voyager = "1.1.0-beta03"
room = "2.7.1"
ksp = "2.3.6"
nav3Core = "1.0.0" nav3Core = "1.0.0"
lifeCycleViewmodelNav3 = "2.10.0" lifeCycleViewmodelNav3 = "2.10.0"
@@ -40,15 +42,22 @@ androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecy
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" } androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
# Voyager
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
# Room
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" } jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
[bundles] [bundles]
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"] voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]