fix: reduce default sync polling interval to improve responsiveness in PreferencesManager
fix: update placeholder in SettingsScreen to reflect new sync interval and enhance user clarity fix: implement date formatting function in webRoutes for consistent timestamp display
This commit is contained in:
+1
-1
@@ -151,7 +151,7 @@ class PreferencesManager(context: Context) {
|
|||||||
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 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 = 5000L
|
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"
|
||||||
|
|||||||
+23
-10
@@ -153,9 +153,24 @@ fun ListsScreen(
|
|||||||
// Maintain a working list as a mutable state list for smoother edge animations
|
// Maintain a working list as a mutable state list for smoother edge animations
|
||||||
val working = remember { androidx.compose.runtime.mutableStateListOf<CollabList>() }
|
val working = remember { androidx.compose.runtime.mutableStateListOf<CollabList>() }
|
||||||
var dragging by remember { mutableStateOf(false) }
|
var dragging by remember { mutableStateOf(false) }
|
||||||
// Keep working list in sync when not dragging
|
var awaitingDb by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(lists, dragging) {
|
var pendingOrderIds by remember { mutableStateOf<List<String>?>(null) }
|
||||||
if (!dragging) {
|
// Keep working list in sync, but when we just committed a reorder, wait until DB reflects it
|
||||||
|
LaunchedEffect(lists) {
|
||||||
|
val currentIds = lists.map { it.id }
|
||||||
|
val pending = pendingOrderIds
|
||||||
|
if (awaitingDb && pending != null) {
|
||||||
|
if (currentIds == pending) {
|
||||||
|
// DB has caught up; now allow UI to exit dragging and sync lists
|
||||||
|
awaitingDb = false
|
||||||
|
dragging = false
|
||||||
|
working.clear()
|
||||||
|
working.addAll(lists)
|
||||||
|
pendingOrderIds = null
|
||||||
|
}
|
||||||
|
// else: still waiting for DB; do not overwrite working yet
|
||||||
|
} else if (!dragging) {
|
||||||
|
// Normal path: sync working to latest lists
|
||||||
working.clear()
|
working.clear()
|
||||||
working.addAll(lists)
|
working.addAll(lists)
|
||||||
}
|
}
|
||||||
@@ -170,7 +185,6 @@ fun ListsScreen(
|
|||||||
add(to.coerceIn(0, size), removeAt(from))
|
add(to.coerceIn(0, size), removeAt(from))
|
||||||
}
|
}
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val reorderState =
|
val reorderState =
|
||||||
rememberReorderableLazyListState(
|
rememberReorderableLazyListState(
|
||||||
onMove = { from, to ->
|
onMove = { from, to ->
|
||||||
@@ -179,12 +193,11 @@ fun ListsScreen(
|
|||||||
working.move(from.index, to.index)
|
working.move(from.index, to.index)
|
||||||
},
|
},
|
||||||
onDragEnd = { _, _ ->
|
onDragEnd = { _, _ ->
|
||||||
// Commit first, then keep dragging=true briefly so the UI can animate smoothly
|
// Commit and wait for DB to reflect this order before exiting drag state
|
||||||
viewModel.commitReorder(working.map { it.id })
|
val ids = working.map { it.id }
|
||||||
scope.launch {
|
pendingOrderIds = ids
|
||||||
delay(150)
|
awaitingDb = true
|
||||||
dragging = false
|
viewModel.commitReorder(ids)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -255,7 +255,7 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
placeholder = { Text("5000") },
|
placeholder = { Text("250") },
|
||||||
)
|
)
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
val parsed = syncIntervalInput.toLongOrNull()
|
val parsed = syncIntervalInput.toLongOrNull()
|
||||||
@@ -278,7 +278,7 @@ fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = "Current: ${'$'}syncIntervalMs ms (min 250, max 600000)",
|
text = "Current: $syncIntervalMs ms (min 250, max 600000)",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -187,6 +187,21 @@ router.get('/', (req: Request, res: Response) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function fmtDate(v) {
|
||||||
|
if (v === null || v === undefined) return '-';
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isNaN(n)) {
|
||||||
|
const ms = n < 1e12 ? n * 1000 : n; // seconds -> ms if needed
|
||||||
|
const d = new Date(ms);
|
||||||
|
return isNaN(d.getTime()) ? '-' : d.toLocaleString();
|
||||||
|
}
|
||||||
|
const t = Date.parse(v);
|
||||||
|
if (!Number.isNaN(t)) {
|
||||||
|
const d = new Date(t);
|
||||||
|
return isNaN(d.getTime()) ? '-' : d.toLocaleString();
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
// Add cache-busting timestamp to URL
|
// Add cache-busting timestamp to URL
|
||||||
@@ -237,8 +252,8 @@ router.get('/', (req: Request, res: Response) => {
|
|||||||
\${list.isDeleted ? '<span class="badge">DELETED</span>' : ''}
|
\${list.isDeleted ? '<span class="badge">DELETED</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="list-id">ID: \${list.id}</div>
|
<div class="list-id">ID: \${list.id}</div>
|
||||||
<div class="list-id">Created: \${new Date(list.createdAt).toLocaleString()}</div>
|
<div class="list-id">Created: \${fmtDate(list.createdAt)}</div>
|
||||||
<div class="list-id">Updated: \${new Date(list.updatedAt).toLocaleString()}</div>
|
<div class="list-id">Updated: \${fmtDate(list.updatedAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -269,7 +284,7 @@ router.get('/', (req: Request, res: Response) => {
|
|||||||
const value = itemValues.find(v => v.fieldId === field.id);
|
const value = itemValues.find(v => v.fieldId === field.id);
|
||||||
return \`<td>\${value ? value.value : '-'}</td>\`;
|
return \`<td>\${value ? value.value : '-'}</td>\`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
<td>\${new Date(item.updatedAt).toLocaleString()}</td>
|
<td>\${fmtDate(item.updatedAt)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
\`;
|
\`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
@@ -313,37 +328,61 @@ router.get('/web/data', async (req: Request, res: Response) => {
|
|||||||
const values = await dbAdapter.queryAll('SELECT * FROM item_values');
|
const values = await dbAdapter.queryAll('SELECT * FROM item_values');
|
||||||
|
|
||||||
// Convert isDeleted from INTEGER to BOOLEAN
|
// Convert isDeleted from INTEGER to BOOLEAN
|
||||||
// Coerce timestamps to numbers (pg may return strings)
|
// Normalize timestamps to milliseconds since epoch
|
||||||
const toNum = (v: any) => (typeof v === 'string' ? Number(v) : v);
|
const toMillis = (v: any): number | null => {
|
||||||
|
if (v === null || v === undefined) return null;
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
return v < 1e12 ? Math.trunc(v * 1000) : Math.trunc(v);
|
||||||
|
}
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
if (/^\d+$/.test(v)) {
|
||||||
|
const n = Number(v);
|
||||||
|
return n < 1e12 ? Math.trunc(n * 1000) : Math.trunc(n);
|
||||||
|
}
|
||||||
|
const t = Date.parse(v);
|
||||||
|
return Number.isNaN(t) ? null : Math.trunc(t);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
// Normalize key casing across SQLite (camelCase) and Postgres (lowercase)
|
||||||
|
const pick = (obj: any, a: string, b: string) => obj[a] ?? obj[b];
|
||||||
const formattedLists = (lists as any[]).map(list => ({
|
const formattedLists = (lists as any[]).map(list => ({
|
||||||
...list,
|
...list,
|
||||||
isDeleted: !!list.isDeleted,
|
isDeleted: !!pick(list, 'isDeleted', 'isdeleted'),
|
||||||
createdAt: toNum(list.createdAt),
|
createdAt: toMillis(pick(list, 'createdAt', 'createdat')),
|
||||||
updatedAt: toNum(list.updatedAt)
|
updatedAt: toMillis(pick(list, 'updatedAt', 'updatedat'))
|
||||||
}));
|
}));
|
||||||
const formattedFields = (fields as any[]).map(field => ({
|
const formattedFields = (fields as any[]).map(field => ({
|
||||||
...field,
|
...field,
|
||||||
isDeleted: !!field.isDeleted,
|
listId: pick(field, 'listId', 'listid'),
|
||||||
createdAt: toNum(field.createdAt),
|
isDeleted: !!pick(field, 'isDeleted', 'isdeleted'),
|
||||||
updatedAt: toNum(field.updatedAt)
|
createdAt: toMillis(pick(field, 'createdAt', 'createdat')),
|
||||||
|
updatedAt: toMillis(pick(field, 'updatedAt', 'updatedat'))
|
||||||
}));
|
}));
|
||||||
const formattedItems = (items as any[]).map(item => ({
|
const formattedItems = (items as any[]).map(item => ({
|
||||||
...item,
|
...item,
|
||||||
isDeleted: !!item.isDeleted,
|
listId: pick(item, 'listId', 'listid'),
|
||||||
createdAt: toNum(item.createdAt),
|
isDeleted: !!pick(item, 'isDeleted', 'isdeleted'),
|
||||||
updatedAt: toNum(item.updatedAt)
|
createdAt: toMillis(pick(item, 'createdAt', 'createdat')),
|
||||||
|
updatedAt: toMillis(pick(item, 'updatedAt', 'updatedat'))
|
||||||
|
}));
|
||||||
|
const formattedValues = (values as any[]).map(v => ({
|
||||||
|
...v,
|
||||||
|
itemId: pick(v, 'itemId', 'itemid'),
|
||||||
|
fieldId: pick(v, 'fieldId', 'fieldid'),
|
||||||
|
updatedAt: toMillis(pick(v, 'updatedAt', 'updatedat'))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
lists: formattedLists,
|
lists: formattedLists,
|
||||||
fields: formattedFields,
|
fields: formattedFields,
|
||||||
items: formattedItems,
|
items: formattedItems,
|
||||||
values,
|
values: formattedValues,
|
||||||
stats: {
|
stats: {
|
||||||
lists: formattedLists.length,
|
lists: formattedLists.length,
|
||||||
fields: formattedFields.length,
|
fields: formattedFields.length,
|
||||||
items: formattedItems.length,
|
items: formattedItems.length,
|
||||||
values: (values as any[]).length
|
values: formattedValues.length
|
||||||
},
|
},
|
||||||
timestamp: Date.now() // Add timestamp for debugging
|
timestamp: Date.now() // Add timestamp for debugging
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user