feat: add persistent column width management and horizontal scrolling enhancements in ListDetailScreen

This commit is contained in:
2025-10-29 12:13:38 +01:00
parent 4955ad0269
commit 72ac97118b
2 changed files with 72 additions and 7 deletions
@@ -2,6 +2,7 @@ package com.collabtable.app.data.preferences
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import org.json.JSONObject
import kotlinx.coroutines.flow.MutableStateFlow 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
@@ -140,6 +141,38 @@ class PreferencesManager(context: Context) {
_syncPollIntervalMs.value = clamped _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<String, Float> {
val key = COLUMN_WIDTHS_PREFIX + listId
val raw = prefs.getString(key, null) ?: return emptyMap()
return try {
val json = JSONObject(raw)
val map = mutableMapOf<String, Float>()
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<String, Float>) {
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 { companion object {
private const val KEY_SERVER_URL = "server_url" private const val KEY_SERVER_URL = "server_url"
private const val KEY_FIRST_RUN = "first_run" private const val KEY_FIRST_RUN = "first_run"
@@ -150,6 +183,7 @@ class PreferencesManager(context: Context) {
private const val KEY_AMOLED_DARK = "amoled_dark" 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 KEY_SYNC_POLL_INTERVAL_MS = "sync_poll_interval_ms" 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_SERVER_URL = "http://10.0.2.2:3000/api/"
private const val DEFAULT_SYNC_POLL_INTERVAL_MS = 250L private const val DEFAULT_SYNC_POLL_INTERVAL_MS = 250L
private const val MIN_SYNC_POLL_INTERVAL_MS = 250L private const val MIN_SYNC_POLL_INTERVAL_MS = 250L
@@ -103,6 +103,7 @@ fun ListDetailScreen(
val context = LocalContext.current val context = LocalContext.current
val database = remember { CollabTableDatabase.getDatabase(context) } val database = remember { CollabTableDatabase.getDatabase(context) }
val viewModel = remember { ListDetailViewModel(database, listId, context) } val viewModel = remember { ListDetailViewModel(database, listId, context) }
val prefs = remember { PreferencesManager.getInstance(context) }
val list by viewModel.list.collectAsState() val list by viewModel.list.collectAsState()
val fields by viewModel.fields.collectAsState() val fields by viewModel.fields.collectAsState()
@@ -129,16 +130,19 @@ fun ListDetailScreen(
// Shared scroll state for synchronized horizontal scrolling // Shared scroll state for synchronized horizontal scrolling
val horizontalScrollState = rememberScrollState() val horizontalScrollState = rememberScrollState()
val scope = rememberCoroutineScope()
// Field widths state (resizable columns) // Field widths state (resizable columns)
val fieldWidths = remember { mutableStateMapOf<String, Dp>() } val fieldWidths = remember { mutableStateMapOf<String, Dp>() }
// Initialize field widths // Initialize field widths (load persisted per-list widths, fallback to default)
LaunchedEffect(stableFields) { LaunchedEffect(stableFields) {
// Load saved widths for this listId
val saved = prefs.getColumnWidths(listId)
stableFields.forEach { field -> stableFields.forEach { field ->
if (!fieldWidths.containsKey(field.id)) { val savedWidth = saved[field.id]?.coerceAtLeast(100f)
fieldWidths[field.id] = 150.dp 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) // Enforce only a minimum width; remove the previous max (400dp)
val newWidth = (currentWidth.value + delta).coerceAtLeast(100f) val newWidth = (currentWidth.value + delta).coerceAtLeast(100f)
fieldWidths[field.id] = newWidth.dp fieldWidths[field.id] = newWidth.dp
// Persist updated widths for this listId
prefs.setColumnWidths(
listId,
fieldWidths.mapValues { it.value.value },
)
}, },
scrollState = horizontalScrollState, scrollState = horizontalScrollState,
isLast = (field.id == stableFields.lastOrNull()?.id), 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()) }
}
},
)
}
} }
} }
} }