feat: add persistent column width management and horizontal scrolling enhancements in ListDetailScreen
This commit is contained in:
+37
-3
@@ -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,10 +183,11 @@ 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
|
||||||
private const val MAX_SYNC_POLL_INTERVAL_MS = 600_000L // 10 minutes
|
private const val MAX_SYNC_POLL_INTERVAL_MS = 600_000L // 10 minutes
|
||||||
const val THEME_MODE_SYSTEM = "system"
|
const val THEME_MODE_SYSTEM = "system"
|
||||||
const val THEME_MODE_LIGHT = "light"
|
const val THEME_MODE_LIGHT = "light"
|
||||||
const val THEME_MODE_DARK = "dark"
|
const val THEME_MODE_DARK = "dark"
|
||||||
|
|||||||
+35
-4
@@ -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()) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user