diff --git a/CollabTableAndroid/.editorconfig b/CollabTableAndroid/.editorconfig new file mode 100644 index 0000000..4848de2 --- /dev/null +++ b/CollabTableAndroid/.editorconfig @@ -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 diff --git a/CollabTableAndroid/app/build.gradle b/CollabTableAndroid/app/build.gradle index 4fdf57c..30b9cce 100644 --- a/CollabTableAndroid/app/build.gradle +++ b/CollabTableAndroid/app/build.gradle @@ -105,3 +105,8 @@ detekt { buildUponDefaultConfig = true allRules = false } + +// Ensure Detekt uses a supported JVM target +tasks.withType(io.gitlab.arturbosch.detekt.Detekt).configureEach { + jvmTarget = "21" +} diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt index ef52f6e..b17463a 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt @@ -1,6 +1,10 @@ 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 kotlinx.coroutines.flow.Flow diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt index ab14ea6..af1b6af 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt @@ -1,6 +1,10 @@ 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.ItemWithValues import kotlinx.coroutines.flow.Flow diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemValueDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemValueDao.kt index 20d084d..ebe5840 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemValueDao.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemValueDao.kt @@ -1,6 +1,9 @@ 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 kotlinx.coroutines.flow.Flow diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ListDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ListDao.kt index cf624cd..806a11d 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ListDao.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ListDao.kt @@ -1,6 +1,10 @@ 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.ListWithFields import kotlinx.coroutines.flow.Flow diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/database/CollabTableDatabase.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/database/CollabTableDatabase.kt index 325f9fc..a9aa61c 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/database/CollabTableDatabase.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/database/CollabTableDatabase.kt @@ -6,10 +6,16 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import com.collabtable.app.data.dao.* -import com.collabtable.app.data.model.* +import com.collabtable.app.data.dao.FieldDao +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) { override fun migrate(database: SupportSQLiteDatabase) { 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) { override fun migrate(database: SupportSQLiteDatabase) { // Local-only column for manual reordering; nullable so existing rows remain unaffected @@ -46,28 +52,28 @@ abstract class CollabTableDatabase : RoomDatabase() { companion object { @Volatile - private var INSTANCE: CollabTableDatabase? = null + private var dbInstance: CollabTableDatabase? = null fun getDatabase(context: Context): CollabTableDatabase { - return INSTANCE ?: synchronized(this) { + return dbInstance ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, CollabTableDatabase::class.java, "collab_table_database", ) - .addMigrations(MIGRATION_1_2, MIGRATION_2_3) + .addMigrations(migration1To2, migration2To3) .build() - INSTANCE = instance + dbInstance = instance instance } } fun clearDatabase(context: Context) { synchronized(this) { - INSTANCE?.close() + dbInstance?.close() context.deleteDatabase("collab_table_database") - INSTANCE = null + dbInstance = null } } } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt index dcf29be..cee1837 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt @@ -61,7 +61,8 @@ data class Field( val listId: String, val name: String, 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 createdAt: Long, val updatedAt: Long, diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt index e39233b..f23be5e 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt @@ -132,17 +132,17 @@ class PreferencesManager(context: Context) { private const val KEY_THEME_MODE = "theme_mode" // system|light|dark private const val KEY_DYNAMIC_COLOR = "dynamic_color" private const val KEY_AMOLED_DARK = "amoled_dark" - private const val KEY_SORT_ORDER = "sort_order" + private const val KEY_SORT_ORDER = "sort_order" private const val DEFAULT_SERVER_URL = "http://10.0.2.2:3000/api/" const val THEME_MODE_SYSTEM = "system" const val THEME_MODE_LIGHT = "light" const val THEME_MODE_DARK = "dark" - // Sort orders - const val SORT_UPDATED_DESC = "updated_desc" // Newest first (default) - const val SORT_UPDATED_ASC = "updated_asc" // Oldest first - const val SORT_NAME_ASC = "name_asc" // A-Z - const val SORT_NAME_DESC = "name_desc" // Z-A + // Sort orders + const val SORT_UPDATED_DESC = "updated_desc" // Newest first (default) + const val SORT_UPDATED_ASC = "updated_asc" // Oldest first + const val SORT_NAME_ASC = "name_asc" // A-Z + const val SORT_NAME_DESC = "name_desc" // Z-A @Volatile private var instance: PreferencesManager? = null diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt index f66b845..01ec35e 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt @@ -44,7 +44,9 @@ class SyncRepository(context: Context) { if (localTotal > 0) { Logger.i( "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()!! } // 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) { Logger.i( "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 database.withTransaction { // 1) Upsert lists first, preserving local orderIndex when present - val idOrderMap = database.listDao().getListIdsAndOrder().associate { it.id to it.orderIndex } - val listsPreservingOrder = syncResponse.lists.map { incoming -> - val localOrder = idOrderMap[incoming.id] - if (localOrder != null) incoming.copy(orderIndex = localOrder) else incoming + val localIdToOrder = database.listDao().getListIdsAndOrder().associate { it.id to it.orderIndex } + // Build a map of local updatedAt to avoid overwriting newer local changes with older server data + 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 (listsPreservingOrder.isNotEmpty()) { + database.listDao().insertLists(listsPreservingOrder) } - database.listDao().insertLists(listsPreservingOrder) // Build set of existing list ids to guard child inserts val existingListIds = database.listDao().getAllListIds().toSet() diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt index 914eafa..e5417fd 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt @@ -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.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectDragGestures 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.items 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.verticalScroll 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.DatePicker -import androidx.compose.material3.DatePickerDialog -import androidx.compose.material3.TimePicker -import androidx.compose.material3.rememberDatePickerState -import androidx.compose.material3.rememberTimePickerState -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +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.Modifier 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.model.Field 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.util.* +import java.util.Calendar +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable @@ -2464,12 +2503,21 @@ fun ManageColumnsDialog( 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( modifier = Modifier .weight(1f) - .fillMaxWidth(), + .fillMaxWidth() + .reorderable(reorderState), + state = reorderState.listState, contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -2477,25 +2525,35 @@ fun ManageColumnsDialog( items = reorderedFields, key = { _, field -> field.id }, ) { index, field -> - ColumnItem( - field = field, - onEdit = { fieldToEdit = field }, - onDelete = { fieldToDelete = field }, - onMoveUp = { - if (index > 0) { - val item = reorderedFields.removeAt(index) - reorderedFields.add(index - 1, item) - } - }, - onMoveDown = { - if (index < reorderedFields.size - 1) { - val item = reorderedFields.removeAt(index) - reorderedFields.add(index + 1, item) - } - }, - canMoveUp = index > 0, - canMoveDown = index < reorderedFields.size - 1, - ) + ReorderableItem(reorderState, key = field.id) { _ -> + ColumnItem( + field = field, + onEdit = { fieldToEdit = field }, + onDelete = { fieldToDelete = field }, + onMoveUp = { + if (index > 0) { + val item = reorderedFields.removeAt(index) + reorderedFields.add(index - 1, item) + } + }, + onMoveDown = { + if (index < reorderedFields.size - 1) { + val item = reorderedFields.removeAt(index) + reorderedFields.add(index + 1, item) + } + }, + canMoveUp = index > 0, + 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, canMoveUp: Boolean, canMoveDown: Boolean, + dragHandle: (@Composable () -> Unit)? = null, ) { Card( modifier = Modifier.fillMaxWidth(), @@ -2604,39 +2663,44 @@ fun ColumnItem( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - // Move up/down buttons - Column { - IconButton( - onClick = onMoveUp, - enabled = canMoveUp, - modifier = Modifier.size(32.dp), - ) { - Icon( - Icons.Default.KeyboardArrowUp, - contentDescription = "Move up", - tint = - if (canMoveUp) { - MaterialTheme.colorScheme.onSurfaceVariant - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) - }, - ) + // Drag handle and optional move buttons + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if (dragHandle != null) { + dragHandle() } - IconButton( - onClick = onMoveDown, - enabled = canMoveDown, - modifier = Modifier.size(32.dp), - ) { - Icon( - Icons.Default.KeyboardArrowDown, - contentDescription = "Move down", - tint = - if (canMoveDown) { - MaterialTheme.colorScheme.onSurfaceVariant - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) - }, - ) + Row { + IconButton( + onClick = onMoveUp, + enabled = canMoveUp, + modifier = Modifier.size(32.dp), + ) { + Icon( + Icons.Default.KeyboardArrowUp, + contentDescription = "Move up", + tint = + if (canMoveUp) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + }, + ) + } + IconButton( + onClick = onMoveDown, + enabled = canMoveDown, + modifier = Modifier.size(32.dp), + ) { + Icon( + Icons.Default.KeyboardArrowDown, + contentDescription = "Move down", + tint = + if (canMoveDown) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + }, + ) + } } } @@ -2662,8 +2726,10 @@ fun ColumnItem( // Selection types com.collabtable.app.data.model.FieldType.CHECKBOX -> "Checkbox" 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.AUTOCOMPLETE -> "Autocomplete (${field.getAutocompleteOptions().size} options)" + com.collabtable.app.data.model.FieldType.DROPDOWN -> + "Dropdown (${field.getDropdownOptions().size} options)" + com.collabtable.app.data.model.FieldType.AUTOCOMPLETE -> + "Autocomplete (${field.getAutocompleteOptions().size} options)" // Link types com.collabtable.app.data.model.FieldType.URL -> "URL" diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt index 7d004e1..96d8a18 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt @@ -5,7 +5,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.room.withTransaction 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 kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt index 1769f18..026bf7a 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt @@ -15,8 +15,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons 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.DragHandle import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.List 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.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 @@ -144,23 +145,56 @@ fun ListsScreen( ) } } else { - val reorderState = rememberReorderableLazyListState(onMove = { from, to -> - val fromIdx = from.index - val toIdx = to.index - if (fromIdx != toIdx) { - viewModel.reorder(fromIdx, toIdx) - } - }) + // Maintain a working copy for smooth drag animations; commit to DB on drag end + var working by remember { mutableStateOf(lists) } + var dragging by remember { mutableStateOf(false) } + // Keep working list in sync when not dragging + LaunchedEffect(lists, dragging) { + if (!dragging) working = lists + } + + fun MutableList.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( - modifier = Modifier - .fillMaxSize() - .reorderable(reorderState), + modifier = + Modifier + .fillMaxSize() + .reorderable(reorderState), state = reorderState.listState, contentPadding = PaddingValues(16.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 -> ListItem( list = list, @@ -250,9 +284,10 @@ fun ListItem( verticalAlignment = Alignment.CenterVertically, ) { Column( - modifier = Modifier - .weight(1f) - .clickable(onClick = onListClick), + modifier = + Modifier + .weight(1f) + .clickable(onClick = onListClick), ) { Text(text = list.name, style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(2.dp)) @@ -365,7 +400,10 @@ private fun SortMenu(prefs: PreferencesManager) { onDismissRequest = { expanded = false }, ) { @Composable - fun ItemOption(label: String, value: String) { + fun ItemOption( + label: String, + value: String, + ) { androidx.compose.material3.DropdownMenuItem( text = { Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt index a76ba11..ac61682 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt @@ -65,7 +65,10 @@ class ListsViewModel( } } - fun reorder(fromIndex: Int, toIndex: Int) { + fun reorder( + fromIndex: Int, + toIndex: Int, + ) { viewModelScope.launch { val current = _lists.value.toMutableList() if (fromIndex !in current.indices || toIndex !in current.indices) return@launch @@ -88,6 +91,13 @@ class ListsViewModel( } } + fun commitReorder(newOrderIds: List) { + viewModelScope.launch { + val pairs = newOrderIds.mapIndexed { idx, id -> id to idx.toLong() } + database.listDao().updateOrderIndexes(pairs) + } + } + private suspend fun startPeriodicSync() { while (true) { kotlinx.coroutines.delay(5000) // Wait 5 seconds before next sync diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt index 03f103b..cf1e48c 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt @@ -1,7 +1,15 @@ package com.collabtable.app.ui.screens 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.LazyRow 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.Delete import androidx.compose.material.icons.filled.FilterList -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Badge +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.Modifier import androidx.compose.ui.graphics.Color diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ServerSetupScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ServerSetupScreen.kt index e4d21b4..0472a11 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ServerSetupScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ServerSetupScreen.kt @@ -1,14 +1,38 @@ 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.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Button +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.Modifier import androidx.compose.ui.platform.LocalContext diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt index 235d233..ec2adc2 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt @@ -1,14 +1,44 @@ 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.filled.ArrowBack import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.LightMode 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.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.Modifier import androidx.compose.ui.platform.LocalContext @@ -232,7 +262,9 @@ fun SettingsScreen( title = { Text("Leave Server?") }, 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 = { diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/utils/Logger.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/utils/Logger.kt index fad1987..fca4d57 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/utils/Logger.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/utils/Logger.kt @@ -5,7 +5,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale data class LogEntry( val timestamp: Long,