feat: enhance data access objects with additional Room annotations and improve database migration logic
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*.{kt,kts}]
|
||||||
|
ktlint_code_style = ktlint_official
|
||||||
|
indent_size = 4
|
||||||
|
ij_kotlin_allow_trailing_comma = true
|
||||||
|
# KtLint >= 0.49: disable specific rules via fully qualified rule ids
|
||||||
|
ktlint_standard_function-naming = disabled
|
||||||
|
ktlint_standard_max-line-length = enabled
|
||||||
|
max_line_length = 180
|
||||||
|
ij_kotlin_name_count_to_use_star_import = 999
|
||||||
|
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
||||||
@@ -105,3 +105,8 @@ detekt {
|
|||||||
buildUponDefaultConfig = true
|
buildUponDefaultConfig = true
|
||||||
allRules = false
|
allRules = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure Detekt uses a supported JVM target
|
||||||
|
tasks.withType(io.gitlab.arturbosch.detekt.Detekt).configureEach {
|
||||||
|
jvmTarget = "21"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package com.collabtable.app.data.dao
|
package com.collabtable.app.data.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import androidx.room.Upsert
|
||||||
import com.collabtable.app.data.model.Field
|
import com.collabtable.app.data.model.Field
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package com.collabtable.app.data.dao
|
package com.collabtable.app.data.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import androidx.room.Upsert
|
||||||
import com.collabtable.app.data.model.Item
|
import com.collabtable.app.data.model.Item
|
||||||
import com.collabtable.app.data.model.ItemWithValues
|
import com.collabtable.app.data.model.ItemWithValues
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.collabtable.app.data.dao
|
package com.collabtable.app.data.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import androidx.room.Upsert
|
||||||
import com.collabtable.app.data.model.ItemValue
|
import com.collabtable.app.data.model.ItemValue
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package com.collabtable.app.data.dao
|
package com.collabtable.app.data.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import androidx.room.Upsert
|
||||||
import com.collabtable.app.data.model.CollabList
|
import com.collabtable.app.data.model.CollabList
|
||||||
import com.collabtable.app.data.model.ListWithFields
|
import com.collabtable.app.data.model.ListWithFields
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|||||||
+16
-10
@@ -6,10 +6,16 @@ import androidx.room.Room
|
|||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.collabtable.app.data.dao.*
|
import com.collabtable.app.data.dao.FieldDao
|
||||||
import com.collabtable.app.data.model.*
|
import com.collabtable.app.data.dao.ItemDao
|
||||||
|
import com.collabtable.app.data.dao.ItemValueDao
|
||||||
|
import com.collabtable.app.data.dao.ListDao
|
||||||
|
import com.collabtable.app.data.model.CollabList
|
||||||
|
import com.collabtable.app.data.model.Field
|
||||||
|
import com.collabtable.app.data.model.Item
|
||||||
|
import com.collabtable.app.data.model.ItemValue
|
||||||
|
|
||||||
val MIGRATION_1_2 =
|
val migration1To2 =
|
||||||
object : Migration(1, 2) {
|
object : Migration(1, 2) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE fields ADD COLUMN fieldType TEXT NOT NULL DEFAULT 'STRING'")
|
database.execSQL("ALTER TABLE fields ADD COLUMN fieldType TEXT NOT NULL DEFAULT 'STRING'")
|
||||||
@@ -17,7 +23,7 @@ val MIGRATION_1_2 =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_2_3 =
|
val migration2To3 =
|
||||||
object : Migration(2, 3) {
|
object : Migration(2, 3) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
// Local-only column for manual reordering; nullable so existing rows remain unaffected
|
// Local-only column for manual reordering; nullable so existing rows remain unaffected
|
||||||
@@ -46,28 +52,28 @@ abstract class CollabTableDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile
|
@Volatile
|
||||||
private var INSTANCE: CollabTableDatabase? = null
|
private var dbInstance: CollabTableDatabase? = null
|
||||||
|
|
||||||
fun getDatabase(context: Context): CollabTableDatabase {
|
fun getDatabase(context: Context): CollabTableDatabase {
|
||||||
return INSTANCE ?: synchronized(this) {
|
return dbInstance ?: synchronized(this) {
|
||||||
val instance =
|
val instance =
|
||||||
Room.databaseBuilder(
|
Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
CollabTableDatabase::class.java,
|
CollabTableDatabase::class.java,
|
||||||
"collab_table_database",
|
"collab_table_database",
|
||||||
)
|
)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
.addMigrations(migration1To2, migration2To3)
|
||||||
.build()
|
.build()
|
||||||
INSTANCE = instance
|
dbInstance = instance
|
||||||
instance
|
instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearDatabase(context: Context) {
|
fun clearDatabase(context: Context) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
INSTANCE?.close()
|
dbInstance?.close()
|
||||||
context.deleteDatabase("collab_table_database")
|
context.deleteDatabase("collab_table_database")
|
||||||
INSTANCE = null
|
dbInstance = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ data class Field(
|
|||||||
val listId: String,
|
val listId: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val fieldType: String = "TEXT",
|
val fieldType: String = "TEXT",
|
||||||
val fieldOptions: String = "", // JSON string for dropdown options, currency symbol, etc.
|
// JSON string for dropdown options, currency symbol, etc.
|
||||||
|
val fieldOptions: String = "",
|
||||||
val order: Int,
|
val order: Int,
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
val updatedAt: Long,
|
val updatedAt: Long,
|
||||||
|
|||||||
+26
-6
@@ -44,7 +44,9 @@ class SyncRepository(context: Context) {
|
|||||||
if (localTotal > 0) {
|
if (localTotal > 0) {
|
||||||
Logger.i(
|
Logger.i(
|
||||||
"Sync",
|
"Sync",
|
||||||
"[OUT] Sending changes (lists=${localLists.size}, fields=${localFields.size}, items=${localItems.size}, values=${localValues.size})",
|
"[OUT] Sending changes " +
|
||||||
|
"(lists=${localLists.size}, fields=${localFields.size}, " +
|
||||||
|
"items=${localItems.size}, values=${localValues.size})",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,23 +80,41 @@ class SyncRepository(context: Context) {
|
|||||||
response.body()!!
|
response.body()!!
|
||||||
}
|
}
|
||||||
// Only log receive when there are actual server changes
|
// Only log receive when there are actual server changes
|
||||||
val inTotal = syncResponse.lists.size + syncResponse.fields.size + syncResponse.items.size + syncResponse.itemValues.size
|
val inTotal =
|
||||||
|
syncResponse.lists.size +
|
||||||
|
syncResponse.fields.size +
|
||||||
|
syncResponse.items.size +
|
||||||
|
syncResponse.itemValues.size
|
||||||
if (inTotal > 0) {
|
if (inTotal > 0) {
|
||||||
Logger.i(
|
Logger.i(
|
||||||
"Sync",
|
"Sync",
|
||||||
"[IN] Received changes (lists=${syncResponse.lists.size}, fields=${syncResponse.fields.size}, items=${syncResponse.items.size}, values=${syncResponse.itemValues.size})",
|
"[IN] Received changes " +
|
||||||
|
"(lists=${syncResponse.lists.size}, fields=${syncResponse.fields.size}, " +
|
||||||
|
"items=${syncResponse.items.size}, values=${syncResponse.itemValues.size})",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply server changes to local database atomically in correct order
|
// Apply server changes to local database atomically in correct order
|
||||||
database.withTransaction {
|
database.withTransaction {
|
||||||
// 1) Upsert lists first, preserving local orderIndex when present
|
// 1) Upsert lists first, preserving local orderIndex when present
|
||||||
val idOrderMap = database.listDao().getListIdsAndOrder().associate { it.id to it.orderIndex }
|
val localIdToOrder = database.listDao().getListIdsAndOrder().associate { it.id to it.orderIndex }
|
||||||
val listsPreservingOrder = syncResponse.lists.map { incoming ->
|
// Build a map of local updatedAt to avoid overwriting newer local changes with older server data
|
||||||
val localOrder = idOrderMap[incoming.id]
|
val localUpdatedMap = database.listDao().getListsUpdatedSince(0).associateBy({ it.id }, { it.updatedAt })
|
||||||
|
|
||||||
|
val listsPreservingOrder =
|
||||||
|
syncResponse.lists
|
||||||
|
.filter { incoming ->
|
||||||
|
val localUpdated = localUpdatedMap[incoming.id]
|
||||||
|
localUpdated == null || incoming.updatedAt >= localUpdated
|
||||||
|
}
|
||||||
|
.map { incoming ->
|
||||||
|
val localOrder = localIdToOrder[incoming.id]
|
||||||
if (localOrder != null) incoming.copy(orderIndex = localOrder) else incoming
|
if (localOrder != null) incoming.copy(orderIndex = localOrder) else incoming
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (listsPreservingOrder.isNotEmpty()) {
|
||||||
database.listDao().insertLists(listsPreservingOrder)
|
database.listDao().insertLists(listsPreservingOrder)
|
||||||
|
}
|
||||||
|
|
||||||
// Build set of existing list ids to guard child inserts
|
// Build set of existing list ids to guard child inserts
|
||||||
val existingListIds = database.listDao().getAllListIds().toSet()
|
val existingListIds = database.listDao().getAllListIds().toSet()
|
||||||
|
|||||||
+82
-16
@@ -1,12 +1,26 @@
|
|||||||
package com.collabtable.app.ui.screens
|
@file:Suppress("ktlint:standard:no-wildcard-imports")
|
||||||
|
|
||||||
|
package com.collabtable.app.ui.screens
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
@@ -16,14 +30,34 @@ import androidx.compose.foundation.text.ClickableText
|
|||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.AccountBox
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.DateRange
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material.icons.filled.List
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.DatePicker
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.material3.DatePickerDialog
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.material3.TimePicker
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.material3.rememberDatePickerState
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.material3.rememberTimePickerState
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
@@ -45,8 +79,13 @@ import com.collabtable.app.R
|
|||||||
import com.collabtable.app.data.database.CollabTableDatabase
|
import com.collabtable.app.data.database.CollabTableDatabase
|
||||||
import com.collabtable.app.data.model.Field
|
import com.collabtable.app.data.model.Field
|
||||||
import com.collabtable.app.data.model.ItemWithValues
|
import com.collabtable.app.data.model.ItemWithValues
|
||||||
|
import org.burnoutcrew.reorderable.ReorderableItem
|
||||||
|
import org.burnoutcrew.reorderable.detectReorder
|
||||||
|
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||||
|
import org.burnoutcrew.reorderable.reorderable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -2464,12 +2503,21 @@ fun ManageColumnsDialog(
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
// Column list
|
// Column list with drag-and-drop reordering
|
||||||
|
val reorderState =
|
||||||
|
rememberReorderableLazyListState(onMove = { from, to ->
|
||||||
|
val item = reorderedFields.removeAt(from.index)
|
||||||
|
val target = if (to.index > reorderedFields.size) reorderedFields.size else to.index
|
||||||
|
reorderedFields.add(target, item)
|
||||||
|
})
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.reorderable(reorderState),
|
||||||
|
state = reorderState.listState,
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
@@ -2477,6 +2525,7 @@ fun ManageColumnsDialog(
|
|||||||
items = reorderedFields,
|
items = reorderedFields,
|
||||||
key = { _, field -> field.id },
|
key = { _, field -> field.id },
|
||||||
) { index, field ->
|
) { index, field ->
|
||||||
|
ReorderableItem(reorderState, key = field.id) { _ ->
|
||||||
ColumnItem(
|
ColumnItem(
|
||||||
field = field,
|
field = field,
|
||||||
onEdit = { fieldToEdit = field },
|
onEdit = { fieldToEdit = field },
|
||||||
@@ -2495,7 +2544,16 @@ fun ManageColumnsDialog(
|
|||||||
},
|
},
|
||||||
canMoveUp = index > 0,
|
canMoveUp = index > 0,
|
||||||
canMoveDown = index < reorderedFields.size - 1,
|
canMoveDown = index < reorderedFields.size - 1,
|
||||||
|
dragHandle = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.DragHandle,
|
||||||
|
contentDescription = "Reorder",
|
||||||
|
modifier = Modifier.detectReorder(reorderState),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2587,6 +2645,7 @@ fun ColumnItem(
|
|||||||
onMoveDown: () -> Unit,
|
onMoveDown: () -> Unit,
|
||||||
canMoveUp: Boolean,
|
canMoveUp: Boolean,
|
||||||
canMoveDown: Boolean,
|
canMoveDown: Boolean,
|
||||||
|
dragHandle: (@Composable () -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -2604,8 +2663,12 @@ fun ColumnItem(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
// Move up/down buttons
|
// Drag handle and optional move buttons
|
||||||
Column {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
if (dragHandle != null) {
|
||||||
|
dragHandle()
|
||||||
|
}
|
||||||
|
Row {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onMoveUp,
|
onClick = onMoveUp,
|
||||||
enabled = canMoveUp,
|
enabled = canMoveUp,
|
||||||
@@ -2639,6 +2702,7 @@ fun ColumnItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
@@ -2662,8 +2726,10 @@ fun ColumnItem(
|
|||||||
// Selection types
|
// Selection types
|
||||||
com.collabtable.app.data.model.FieldType.CHECKBOX -> "Checkbox"
|
com.collabtable.app.data.model.FieldType.CHECKBOX -> "Checkbox"
|
||||||
com.collabtable.app.data.model.FieldType.SWITCH -> "Switch"
|
com.collabtable.app.data.model.FieldType.SWITCH -> "Switch"
|
||||||
com.collabtable.app.data.model.FieldType.DROPDOWN -> "Dropdown (${field.getDropdownOptions().size} options)"
|
com.collabtable.app.data.model.FieldType.DROPDOWN ->
|
||||||
com.collabtable.app.data.model.FieldType.AUTOCOMPLETE -> "Autocomplete (${field.getAutocompleteOptions().size} options)"
|
"Dropdown (${field.getDropdownOptions().size} options)"
|
||||||
|
com.collabtable.app.data.model.FieldType.AUTOCOMPLETE ->
|
||||||
|
"Autocomplete (${field.getAutocompleteOptions().size} options)"
|
||||||
|
|
||||||
// Link types
|
// Link types
|
||||||
com.collabtable.app.data.model.FieldType.URL -> "URL"
|
com.collabtable.app.data.model.FieldType.URL -> "URL"
|
||||||
|
|||||||
+5
-1
@@ -5,7 +5,11 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import com.collabtable.app.data.database.CollabTableDatabase
|
import com.collabtable.app.data.database.CollabTableDatabase
|
||||||
import com.collabtable.app.data.model.*
|
import com.collabtable.app.data.model.CollabList
|
||||||
|
import com.collabtable.app.data.model.Field
|
||||||
|
import com.collabtable.app.data.model.Item
|
||||||
|
import com.collabtable.app.data.model.ItemValue
|
||||||
|
import com.collabtable.app.data.model.ItemWithValues
|
||||||
import com.collabtable.app.data.repository.SyncRepository
|
import com.collabtable.app.data.repository.SyncRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|||||||
+49
-11
@@ -15,8 +15,8 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
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.DragHandle
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.List
|
import androidx.compose.material.icons.filled.List
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
@@ -37,6 +37,7 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -144,23 +145,56 @@ fun ListsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val reorderState = rememberReorderableLazyListState(onMove = { from, to ->
|
// Maintain a working copy for smooth drag animations; commit to DB on drag end
|
||||||
val fromIdx = from.index
|
var working by remember { mutableStateOf(lists) }
|
||||||
val toIdx = to.index
|
var dragging by remember { mutableStateOf(false) }
|
||||||
if (fromIdx != toIdx) {
|
// Keep working list in sync when not dragging
|
||||||
viewModel.reorder(fromIdx, toIdx)
|
LaunchedEffect(lists, dragging) {
|
||||||
|
if (!dragging) working = lists
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
fun MutableList<CollabList>.move(
|
||||||
|
from: Int,
|
||||||
|
to: Int,
|
||||||
|
) {
|
||||||
|
if (from == to) return
|
||||||
|
val item = removeAt(from)
|
||||||
|
add(
|
||||||
|
if (to > size) {
|
||||||
|
size
|
||||||
|
} else if (to < 0) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
to
|
||||||
|
},
|
||||||
|
item,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val reorderState =
|
||||||
|
rememberReorderableLazyListState(
|
||||||
|
onMove = { from, to ->
|
||||||
|
dragging = true
|
||||||
|
val newList = working.toMutableList()
|
||||||
|
newList.move(from.index, to.index)
|
||||||
|
working = newList
|
||||||
|
},
|
||||||
|
onDragEnd = { _, _ ->
|
||||||
|
dragging = false
|
||||||
|
viewModel.commitReorder(working.map { it.id })
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.reorderable(reorderState),
|
.reorderable(reorderState),
|
||||||
state = reorderState.listState,
|
state = reorderState.listState,
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(lists, key = { _, it -> it.id }) { index, list ->
|
itemsIndexed(working, key = { _, it -> it.id }) { index, list ->
|
||||||
ReorderableItem(reorderState, key = list.id) { isDragging ->
|
ReorderableItem(reorderState, key = list.id) { isDragging ->
|
||||||
ListItem(
|
ListItem(
|
||||||
list = list,
|
list = list,
|
||||||
@@ -250,7 +284,8 @@ fun ListItem(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clickable(onClick = onListClick),
|
.clickable(onClick = onListClick),
|
||||||
) {
|
) {
|
||||||
@@ -365,7 +400,10 @@ private fun SortMenu(prefs: PreferencesManager) {
|
|||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
) {
|
) {
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemOption(label: String, value: String) {
|
fun ItemOption(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
androidx.compose.material3.DropdownMenuItem(
|
androidx.compose.material3.DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
|||||||
+11
-1
@@ -65,7 +65,10 @@ class ListsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reorder(fromIndex: Int, toIndex: Int) {
|
fun reorder(
|
||||||
|
fromIndex: Int,
|
||||||
|
toIndex: Int,
|
||||||
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val current = _lists.value.toMutableList()
|
val current = _lists.value.toMutableList()
|
||||||
if (fromIndex !in current.indices || toIndex !in current.indices) return@launch
|
if (fromIndex !in current.indices || toIndex !in current.indices) return@launch
|
||||||
@@ -88,6 +91,13 @@ class ListsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun commitReorder(newOrderIds: List<String>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val pairs = newOrderIds.mapIndexed { idx, id -> id to idx.toLong() }
|
||||||
|
database.listDao().updateOrderIndexes(pairs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun startPeriodicSync() {
|
private suspend fun startPeriodicSync() {
|
||||||
while (true) {
|
while (true) {
|
||||||
kotlinx.coroutines.delay(5000) // Wait 5 seconds before next sync
|
kotlinx.coroutines.delay(5000) // Wait 5 seconds before next sync
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
package com.collabtable.app.ui.screens
|
package com.collabtable.app.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -12,8 +20,30 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|||||||
+27
-3
@@ -1,14 +1,38 @@
|
|||||||
package com.collabtable.app.ui.screens
|
package com.collabtable.app.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.Error
|
import androidx.compose.material.icons.filled.Error
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|||||||
+36
-4
@@ -1,14 +1,44 @@
|
|||||||
package com.collabtable.app.ui.screens
|
package com.collabtable.app.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.DarkMode
|
import androidx.compose.material.icons.filled.DarkMode
|
||||||
import androidx.compose.material.icons.filled.LightMode
|
import androidx.compose.material.icons.filled.LightMode
|
||||||
import androidx.compose.material.icons.filled.SettingsBrightness
|
import androidx.compose.material.icons.filled.SettingsBrightness
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -232,7 +262,9 @@ fun SettingsScreen(
|
|||||||
title = { Text("Leave Server?") },
|
title = { Text("Leave Server?") },
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
"All your data will be synced to the server before disconnecting. After leaving, all local data will be deleted and you'll need to set up a new connection. This action cannot be undone.",
|
"All your data will be synced to the server before disconnecting. " +
|
||||||
|
"After leaving, all local data will be deleted and you'll need to set up a new connection. " +
|
||||||
|
"This action cannot be undone.",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
data class LogEntry(
|
data class LogEntry(
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
|
|||||||
Reference in New Issue
Block a user