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 0d473b8..a6e0254 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 @@ -2,6 +2,7 @@ package com.collabtable.app.data.preferences import android.content.Context import android.content.SharedPreferences +import org.json.JSONObject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -140,6 +141,38 @@ class PreferencesManager(context: Context) { _syncPollIntervalMs.value = clamped } + // Persist per-list column widths (fieldId -> widthDp) + // Stored as a JSON object string in SharedPreferences under key: COLUMN_WIDTHS_PREFIX + listId + fun getColumnWidths(listId: String): Map { + val key = COLUMN_WIDTHS_PREFIX + listId + val raw = prefs.getString(key, null) ?: return emptyMap() + return try { + val json = JSONObject(raw) + val map = mutableMapOf() + val it = json.keys() + while (it.hasNext()) { + val fieldId = it.next() + val width = json.optDouble(fieldId, Double.NaN) + if (!width.isNaN()) { + map[fieldId] = width.toFloat() + } + } + map + } catch (e: Exception) { + emptyMap() + } + } + + fun setColumnWidths(listId: String, widthsDp: Map) { + val key = COLUMN_WIDTHS_PREFIX + listId + val json = JSONObject() + widthsDp.forEach { (fieldId, width) -> + // Persist as double for precision, value is in dp units + json.put(fieldId, width.toDouble()) + } + prefs.edit().putString(key, json.toString()).apply() + } + companion object { private const val KEY_SERVER_URL = "server_url" private const val KEY_FIRST_RUN = "first_run" @@ -150,10 +183,11 @@ class PreferencesManager(context: Context) { private const val KEY_AMOLED_DARK = "amoled_dark" private const val KEY_SORT_ORDER = "sort_order" private const val KEY_SYNC_POLL_INTERVAL_MS = "sync_poll_interval_ms" + private const val COLUMN_WIDTHS_PREFIX = "column_widths_" // + listId private const val DEFAULT_SERVER_URL = "http://10.0.2.2:3000/api/" - private const val DEFAULT_SYNC_POLL_INTERVAL_MS = 250L - private const val MIN_SYNC_POLL_INTERVAL_MS = 250L - private const val MAX_SYNC_POLL_INTERVAL_MS = 600_000L // 10 minutes + private const val DEFAULT_SYNC_POLL_INTERVAL_MS = 250L + private const val MIN_SYNC_POLL_INTERVAL_MS = 250L + private const val MAX_SYNC_POLL_INTERVAL_MS = 600_000L // 10 minutes const val THEME_MODE_SYSTEM = "system" const val THEME_MODE_LIGHT = "light" const val THEME_MODE_DARK = "dark" 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 a2ededc..a5ae34b 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 @@ -103,6 +103,7 @@ fun ListDetailScreen( val context = LocalContext.current val database = remember { CollabTableDatabase.getDatabase(context) } val viewModel = remember { ListDetailViewModel(database, listId, context) } + val prefs = remember { PreferencesManager.getInstance(context) } val list by viewModel.list.collectAsState() val fields by viewModel.fields.collectAsState() @@ -129,16 +130,19 @@ fun ListDetailScreen( // Shared scroll state for synchronized horizontal scrolling val horizontalScrollState = rememberScrollState() + val scope = rememberCoroutineScope() // Field widths state (resizable columns) val fieldWidths = remember { mutableStateMapOf() } - // Initialize field widths + // Initialize field widths (load persisted per-list widths, fallback to default) LaunchedEffect(stableFields) { + // Load saved widths for this listId + val saved = prefs.getColumnWidths(listId) stableFields.forEach { field -> - if (!fieldWidths.containsKey(field.id)) { - fieldWidths[field.id] = 150.dp - } + val savedWidth = saved[field.id]?.coerceAtLeast(100f) + val widthDp = (savedWidth ?: 150f).dp + fieldWidths[field.id] = widthDp } } @@ -411,6 +415,11 @@ fun ListDetailScreen( // Enforce only a minimum width; remove the previous max (400dp) val newWidth = (currentWidth.value + delta).coerceAtLeast(100f) fieldWidths[field.id] = newWidth.dp + // Persist updated widths for this listId + prefs.setColumnWidths( + listId, + fieldWidths.mapValues { it.value.value }, + ) }, scrollState = horizontalScrollState, isLast = (field.id == stableFields.lastOrNull()?.id), @@ -487,6 +496,28 @@ fun ListDetailScreen( ) } } + + // Filler area that occupies remaining viewport height and enables horizontal dragging + // to scroll the table even when tapping below the last row on small tables. + item(key = "hscroll_filler") { + Box( + modifier = + Modifier + .fillMaxWidth() + .fillParentMaxHeight() + .pointerInput(horizontalScrollState) { + detectDragGestures { change, dragAmount -> + change.consume() + val max = horizontalScrollState.maxValue.toFloat() + val current = horizontalScrollState.value.toFloat() + val target = (current - dragAmount.x).coerceIn(0f, max) + // Drag right (positive x) should reveal right side (increase scroll) + // We subtract dragAmount.x because scrolling value increases when content moves left. + scope.launch { horizontalScrollState.scrollTo(target.toInt()) } + } + }, + ) + } } } }