feat: enhance code readability by applying consistent formatting and style adjustments across multiple files
This commit is contained in:
@@ -108,6 +108,8 @@ ktlint {
|
||||
detekt {
|
||||
buildUponDefaultConfig = true
|
||||
allRules = false
|
||||
// Point detekt to the repo's configuration file
|
||||
config = files("$rootDir/detekt.yml")
|
||||
}
|
||||
|
||||
// Ensure Detekt uses a supported JVM target
|
||||
|
||||
+35
-50
@@ -7,7 +7,9 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.json.JSONObject
|
||||
|
||||
class PreferencesManager(context: Context) {
|
||||
class PreferencesManager(
|
||||
context: Context,
|
||||
) {
|
||||
private val prefs: SharedPreferences =
|
||||
context.getSharedPreferences("collab_table_prefs", Context.MODE_PRIVATE)
|
||||
|
||||
@@ -40,9 +42,7 @@ class PreferencesManager(context: Context) {
|
||||
private val _notifyListRemoved = MutableStateFlow(isNotifyListRemovedEnabled())
|
||||
val notifyListRemoved: StateFlow<Boolean> = _notifyListRemoved.asStateFlow()
|
||||
|
||||
fun getServerUrl(): String {
|
||||
return prefs.getString(KEY_SERVER_URL, DEFAULT_SERVER_URL) ?: DEFAULT_SERVER_URL
|
||||
}
|
||||
fun getServerUrl(): String = prefs.getString(KEY_SERVER_URL, DEFAULT_SERVER_URL) ?: DEFAULT_SERVER_URL
|
||||
|
||||
fun setServerUrl(url: String) {
|
||||
val raw = url.trim()
|
||||
@@ -57,17 +57,13 @@ class PreferencesManager(context: Context) {
|
||||
_serverUrl.value = cleanUrl
|
||||
}
|
||||
|
||||
fun isFirstRun(): Boolean {
|
||||
return prefs.getBoolean(KEY_FIRST_RUN, true)
|
||||
}
|
||||
fun isFirstRun(): Boolean = prefs.getBoolean(KEY_FIRST_RUN, true)
|
||||
|
||||
fun setIsFirstRun(isFirstRun: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_FIRST_RUN, isFirstRun).apply()
|
||||
}
|
||||
|
||||
fun getServerPassword(): String? {
|
||||
return prefs.getString(KEY_SERVER_PASSWORD, null)
|
||||
}
|
||||
fun getServerPassword(): String? = prefs.getString(KEY_SERVER_PASSWORD, null)
|
||||
|
||||
fun setServerPassword(password: String) {
|
||||
if (password.isBlank()) {
|
||||
@@ -83,7 +79,8 @@ class PreferencesManager(context: Context) {
|
||||
}
|
||||
|
||||
fun clearServerSettings() {
|
||||
prefs.edit()
|
||||
prefs
|
||||
.edit()
|
||||
.remove(KEY_SERVER_URL)
|
||||
.remove(KEY_SERVER_PASSWORD)
|
||||
.remove(KEY_LAST_SYNC_TIMESTAMP)
|
||||
@@ -92,9 +89,7 @@ class PreferencesManager(context: Context) {
|
||||
}
|
||||
|
||||
// Theme settings
|
||||
fun getThemeMode(): String {
|
||||
return prefs.getString(KEY_THEME_MODE, THEME_MODE_SYSTEM) ?: THEME_MODE_SYSTEM
|
||||
}
|
||||
fun getThemeMode(): String = prefs.getString(KEY_THEME_MODE, THEME_MODE_SYSTEM) ?: THEME_MODE_SYSTEM
|
||||
|
||||
fun setThemeMode(mode: String) {
|
||||
val normalized =
|
||||
@@ -106,18 +101,14 @@ class PreferencesManager(context: Context) {
|
||||
_themeMode.value = normalized
|
||||
}
|
||||
|
||||
fun isDynamicColorEnabled(): Boolean {
|
||||
return prefs.getBoolean(KEY_DYNAMIC_COLOR, true)
|
||||
}
|
||||
fun isDynamicColorEnabled(): Boolean = prefs.getBoolean(KEY_DYNAMIC_COLOR, true)
|
||||
|
||||
fun setDynamicColorEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_DYNAMIC_COLOR, enabled).apply()
|
||||
_dynamicColor.value = enabled
|
||||
}
|
||||
|
||||
fun isAmoledDarkEnabled(): Boolean {
|
||||
return prefs.getBoolean(KEY_AMOLED_DARK, false)
|
||||
}
|
||||
fun isAmoledDarkEnabled(): Boolean = prefs.getBoolean(KEY_AMOLED_DARK, false)
|
||||
|
||||
fun setAmoledDarkEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_AMOLED_DARK, enabled).apply()
|
||||
@@ -125,9 +116,7 @@ class PreferencesManager(context: Context) {
|
||||
}
|
||||
|
||||
// Sorting preferences for tables overview
|
||||
fun getSortOrder(): String {
|
||||
return prefs.getString(KEY_SORT_ORDER, SORT_UPDATED_DESC) ?: SORT_UPDATED_DESC
|
||||
}
|
||||
fun getSortOrder(): String = prefs.getString(KEY_SORT_ORDER, SORT_UPDATED_DESC) ?: SORT_UPDATED_DESC
|
||||
|
||||
fun setSortOrder(order: String) {
|
||||
val normalized =
|
||||
@@ -152,36 +141,28 @@ class PreferencesManager(context: Context) {
|
||||
}
|
||||
|
||||
// Last time we checked lists for background notifications
|
||||
fun getLastListNotifyCheckTimestamp(): Long {
|
||||
return prefs.getLong(KEY_LAST_LIST_NOTIFY_CHECK_TS, 0L)
|
||||
}
|
||||
fun getLastListNotifyCheckTimestamp(): Long = prefs.getLong(KEY_LAST_LIST_NOTIFY_CHECK_TS, 0L)
|
||||
|
||||
fun setLastListNotifyCheckTimestamp(ts: Long) {
|
||||
prefs.edit().putLong(KEY_LAST_LIST_NOTIFY_CHECK_TS, ts).apply()
|
||||
}
|
||||
|
||||
// Notification settings accessors
|
||||
private fun isNotifyListAddedEnabled(): Boolean {
|
||||
return prefs.getBoolean(KEY_NOTIFY_LIST_ADDED, true)
|
||||
}
|
||||
private fun isNotifyListAddedEnabled(): Boolean = prefs.getBoolean(KEY_NOTIFY_LIST_ADDED, true)
|
||||
|
||||
fun setNotifyListAddedEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_NOTIFY_LIST_ADDED, enabled).apply()
|
||||
_notifyListAdded.value = enabled
|
||||
}
|
||||
|
||||
private fun isNotifyListEditedEnabled(): Boolean {
|
||||
return prefs.getBoolean(KEY_NOTIFY_LIST_EDITED, true)
|
||||
}
|
||||
private fun isNotifyListEditedEnabled(): Boolean = prefs.getBoolean(KEY_NOTIFY_LIST_EDITED, true)
|
||||
|
||||
fun setNotifyListEditedEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_NOTIFY_LIST_EDITED, enabled).apply()
|
||||
_notifyListEdited.value = enabled
|
||||
}
|
||||
|
||||
private fun isNotifyListRemovedEnabled(): Boolean {
|
||||
return prefs.getBoolean(KEY_NOTIFY_LIST_REMOVED, true)
|
||||
}
|
||||
private fun isNotifyListRemovedEnabled(): Boolean = prefs.getBoolean(KEY_NOTIFY_LIST_REMOVED, true)
|
||||
|
||||
fun setNotifyListRemovedEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_NOTIFY_LIST_REMOVED, enabled).apply()
|
||||
@@ -235,11 +216,12 @@ class PreferencesManager(context: Context) {
|
||||
while (it.hasNext()) {
|
||||
val fieldId = it.next()
|
||||
val align = json.optString(fieldId, "start")
|
||||
val normalized = when (align.lowercase()) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
val normalized =
|
||||
when (align.lowercase()) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
map[fieldId] = normalized
|
||||
}
|
||||
map
|
||||
@@ -248,15 +230,19 @@ class PreferencesManager(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun setColumnAlignments(listId: String, alignments: Map<String, String>) {
|
||||
fun setColumnAlignments(
|
||||
listId: String,
|
||||
alignments: Map<String, String>,
|
||||
) {
|
||||
val key = COLUMN_ALIGN_PREFIX + listId
|
||||
val json = JSONObject()
|
||||
alignments.forEach { (fieldId, alignment) ->
|
||||
val normalized = when (alignment.lowercase()) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
val normalized =
|
||||
when (alignment.lowercase()) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
json.put(fieldId, normalized)
|
||||
}
|
||||
prefs.edit().putString(key, json.toString()).apply()
|
||||
@@ -277,7 +263,7 @@ class PreferencesManager(context: Context) {
|
||||
private const val KEY_NOTIFY_LIST_REMOVED = "notify_list_removed"
|
||||
private const val KEY_LAST_LIST_NOTIFY_CHECK_TS = "last_list_notify_check_ts"
|
||||
private const val COLUMN_WIDTHS_PREFIX = "column_widths_" // + listId
|
||||
private const val COLUMN_ALIGN_PREFIX = "column_align_" // + listId
|
||||
private const val COLUMN_ALIGN_PREFIX = "column_align_" // + 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
|
||||
@@ -295,10 +281,9 @@ class PreferencesManager(context: Context) {
|
||||
@Volatile
|
||||
private var instance: PreferencesManager? = null
|
||||
|
||||
fun getInstance(context: Context): PreferencesManager {
|
||||
return instance ?: synchronized(this) {
|
||||
fun getInstance(context: Context): PreferencesManager =
|
||||
instance ?: synchronized(this) {
|
||||
instance ?: PreferencesManager(context.applicationContext).also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-8
@@ -15,7 +15,9 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.UnknownHostException
|
||||
|
||||
class SyncRepository(context: Context) {
|
||||
class SyncRepository(
|
||||
context: Context,
|
||||
) {
|
||||
private val appContext = context.applicationContext
|
||||
private val database = CollabTableDatabase.getDatabase(appContext)
|
||||
private val api = ApiClient.api
|
||||
@@ -51,7 +53,8 @@ class SyncRepository(context: Context) {
|
||||
}
|
||||
|
||||
private fun setLastServerSyncTs(ts: Long) {
|
||||
prefs.edit()
|
||||
prefs
|
||||
.edit()
|
||||
.putLong("last_server_sync_ts", ts)
|
||||
// also update legacy for safety
|
||||
.putLong("last_sync_timestamp", ts)
|
||||
@@ -128,16 +131,24 @@ class SyncRepository(context: Context) {
|
||||
|
||||
// Gather local changes since last sync
|
||||
val localLists =
|
||||
database.listDao().getListsUpdatedSince(lastLocalTs)
|
||||
database
|
||||
.listDao()
|
||||
.getListsUpdatedSince(lastLocalTs)
|
||||
.filter { it.updatedAt in (lastLocalTs + 1)..localSnapshotTs }
|
||||
val localFields =
|
||||
database.fieldDao().getFieldsUpdatedSince(lastLocalTs)
|
||||
database
|
||||
.fieldDao()
|
||||
.getFieldsUpdatedSince(lastLocalTs)
|
||||
.filter { it.updatedAt in (lastLocalTs + 1)..localSnapshotTs }
|
||||
val localItems =
|
||||
database.itemDao().getItemsUpdatedSince(lastLocalTs)
|
||||
database
|
||||
.itemDao()
|
||||
.getItemsUpdatedSince(lastLocalTs)
|
||||
.filter { it.updatedAt in (lastLocalTs + 1)..localSnapshotTs }
|
||||
val localValues =
|
||||
database.itemValueDao().getValuesUpdatedSince(lastLocalTs)
|
||||
database
|
||||
.itemValueDao()
|
||||
.getValuesUpdatedSince(lastLocalTs)
|
||||
.filter { it.updatedAt in (lastLocalTs + 1)..localSnapshotTs }
|
||||
|
||||
// Only log send when there are actual local changes
|
||||
@@ -220,8 +231,7 @@ class SyncRepository(context: Context) {
|
||||
.filter { incoming ->
|
||||
val localUpdated = localUpdatedMap[incoming.id]
|
||||
localUpdated == null || incoming.updatedAt >= localUpdated
|
||||
}
|
||||
.map { incoming ->
|
||||
}.map { incoming ->
|
||||
val localOrder = localIdToOrder[incoming.id]
|
||||
if (localOrder != null) incoming.copy(orderIndex = localOrder) else incoming
|
||||
}
|
||||
|
||||
+5
-4
@@ -20,14 +20,14 @@ object NotificationHelper {
|
||||
context: Context,
|
||||
title: String,
|
||||
text: String,
|
||||
): NotificationCompat.Builder {
|
||||
return NotificationCompat.Builder(context, CHANNEL_LIST_EVENTS)
|
||||
): NotificationCompat.Builder =
|
||||
NotificationCompat
|
||||
.Builder(context, CHANNEL_LIST_EVENTS)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
private fun notify(
|
||||
context: Context,
|
||||
@@ -79,7 +79,8 @@ object NotificationHelper {
|
||||
inbox.setSummaryText("$totalEventCount total event" + if (totalEventCount == 1) "" else "s")
|
||||
}
|
||||
val summary =
|
||||
NotificationCompat.Builder(context, CHANNEL_LIST_EVENTS)
|
||||
NotificationCompat
|
||||
.Builder(context, CHANNEL_LIST_EVENTS)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle("List activity")
|
||||
.setContentText("Recent list changes")
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.collabtable.app.ui.navigation
|
||||
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -17,10 +21,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
+120
-101
@@ -13,7 +13,6 @@ 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.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -54,47 +53,47 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
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.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
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 com.collabtable.app.data.preferences.PreferencesManager
|
||||
import com.collabtable.app.ui.components.ConnectionStatusAction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.burnoutcrew.reorderable.ReorderableItem
|
||||
import org.burnoutcrew.reorderable.detectReorder
|
||||
@@ -166,11 +165,12 @@ fun ListDetailScreen(
|
||||
val savedAlign = prefs.getColumnAlignments(listId)
|
||||
stableFields.forEach { field ->
|
||||
val a = savedAlign[field.id]?.lowercase() ?: "start"
|
||||
columnAlignments[field.id] = when (a) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
columnAlignments[field.id] =
|
||||
when (a) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,16 +224,17 @@ fun ListDetailScreen(
|
||||
filterField,
|
||||
filterValue,
|
||||
) {
|
||||
value = withContext(Dispatchers.Default) {
|
||||
transformItems(
|
||||
items = stableItems,
|
||||
sortField = sortField,
|
||||
sortAscending = sortAscending,
|
||||
groupByField = groupByField,
|
||||
filterField = filterField,
|
||||
filterValue = filterValue,
|
||||
)
|
||||
}
|
||||
value =
|
||||
withContext(Dispatchers.Default) {
|
||||
transformItems(
|
||||
items = stableItems,
|
||||
sortField = sortField,
|
||||
sortAscending = sortAscending,
|
||||
groupByField = groupByField,
|
||||
filterField = filterField,
|
||||
filterValue = filterValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val processedItems = transformed.processed
|
||||
@@ -414,12 +415,13 @@ fun ListDetailScreen(
|
||||
val v = item.values.find { it.fieldId == field.id }
|
||||
if (v != null && v.updatedAt > maxValUpdated) maxValUpdated = v.updatedAt
|
||||
}
|
||||
val signature = AutoFitSignature(
|
||||
name = field.name,
|
||||
fieldUpdatedAt = field.updatedAt,
|
||||
itemCount = stableItems.size,
|
||||
maxValueUpdatedAt = maxValUpdated,
|
||||
)
|
||||
val signature =
|
||||
AutoFitSignature(
|
||||
name = field.name,
|
||||
fieldUpdatedAt = field.updatedAt,
|
||||
itemCount = stableItems.size,
|
||||
maxValueUpdatedAt = maxValUpdated,
|
||||
)
|
||||
|
||||
val cached = autoFitCache[field.id]
|
||||
if (cached != null && cached.first == signature) {
|
||||
@@ -429,22 +431,31 @@ fun ListDetailScreen(
|
||||
}
|
||||
|
||||
// Measure header and max content width
|
||||
val headerPx = textMeasurer.measure(AnnotatedString(field.name), style = headerTextStyle).size.width.toFloat()
|
||||
val headerPx =
|
||||
textMeasurer
|
||||
.measure(AnnotatedString(field.name), style = headerTextStyle)
|
||||
.size.width
|
||||
.toFloat()
|
||||
var maxContentPx = 0f
|
||||
stableItems.forEach { itemWithValues ->
|
||||
val v = itemWithValues.values.find { it.fieldId == field.id }?.value
|
||||
val display = getDisplayTextForMeasure(field, v)
|
||||
if (display.isNotEmpty()) {
|
||||
val w = textMeasurer.measure(AnnotatedString(display), style = bodyTextStyle).size.width.toFloat()
|
||||
val w =
|
||||
textMeasurer
|
||||
.measure(AnnotatedString(display), style = bodyTextStyle)
|
||||
.size.width
|
||||
.toFloat()
|
||||
if (w > maxContentPx) maxContentPx = w
|
||||
}
|
||||
}
|
||||
val widthDpValue = with(density) {
|
||||
val headerDp = headerPx.toDp() + 12.dp + 12.dp + 24.dp + 2.dp
|
||||
val contentDp = maxContentPx.toDp() + 8.dp + 8.dp + 2.dp
|
||||
val base = maxOf(headerDp, contentDp, 100.dp)
|
||||
(base + 6.dp).value
|
||||
}
|
||||
val widthDpValue =
|
||||
with(density) {
|
||||
val headerDp = headerPx.toDp() + 12.dp + 12.dp + 24.dp + 2.dp
|
||||
val contentDp = maxContentPx.toDp() + 8.dp + 8.dp + 2.dp
|
||||
val base = maxOf(headerDp, contentDp, 100.dp)
|
||||
(base + 6.dp).value
|
||||
}
|
||||
fieldWidths[field.id] = widthDpValue.dp
|
||||
autoFitCache[field.id] = signature to widthDpValue
|
||||
}
|
||||
@@ -591,11 +602,12 @@ fun ListDetailScreen(
|
||||
val savedAlign = prefs.getColumnAlignments(listId)
|
||||
stableFields.forEach { field ->
|
||||
val a = savedAlign[field.id]?.lowercase() ?: columnAlignments[field.id] ?: "start"
|
||||
columnAlignments[field.id] = when (a) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
columnAlignments[field.id] =
|
||||
when (a) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
}
|
||||
},
|
||||
onAddField = { name, fieldType, fieldOptions ->
|
||||
@@ -707,7 +719,10 @@ private data class TransformedItems(
|
||||
val grouped: Map<String, List<ItemWithValues>>,
|
||||
)
|
||||
|
||||
private fun valueFor(item: ItemWithValues, field: Field?): String {
|
||||
private fun valueFor(
|
||||
item: ItemWithValues,
|
||||
field: Field?,
|
||||
): String {
|
||||
if (field == null) return ""
|
||||
return item.values.find { it.fieldId == field.id }?.value ?: ""
|
||||
}
|
||||
@@ -793,8 +808,8 @@ private data class AutoFitSignature(
|
||||
)
|
||||
|
||||
// Centralized mapping from canonical/legacy field type strings to user-facing labels
|
||||
private fun fieldTypeToLabel(type: String): String {
|
||||
return when (type.uppercase()) {
|
||||
private fun fieldTypeToLabel(type: String): String =
|
||||
when (type.uppercase()) {
|
||||
"TEXT", "STRING" -> "Text"
|
||||
"MULTILINE_TEXT" -> "Multi-line Text"
|
||||
"NUMBER" -> "Number"
|
||||
@@ -820,7 +835,6 @@ private fun fieldTypeToLabel(type: String): String {
|
||||
"LOCATION" -> "Location"
|
||||
else -> "Text"
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -863,11 +877,12 @@ fun FieldHeader(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.clickable { onHeaderClick() },
|
||||
textAlign = when (alignment) {
|
||||
"center" -> TextAlign.Center
|
||||
"end" -> TextAlign.End
|
||||
else -> TextAlign.Start
|
||||
},
|
||||
textAlign =
|
||||
when (alignment) {
|
||||
"center" -> TextAlign.Center
|
||||
"end" -> TextAlign.End
|
||||
else -> TextAlign.Start
|
||||
},
|
||||
)
|
||||
|
||||
// Resize handle
|
||||
@@ -939,7 +954,6 @@ fun FieldHeader(
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -972,16 +986,18 @@ fun ItemRow(
|
||||
fields.forEach { field ->
|
||||
key(field.id) {
|
||||
val value = valuesByFieldId[field.id]
|
||||
val alignment = when (fieldAlignments[field.id]?.lowercase()) {
|
||||
"center" -> Alignment.Center
|
||||
"end", "right" -> Alignment.CenterEnd
|
||||
else -> Alignment.CenterStart
|
||||
}
|
||||
val textAlign = when (fieldAlignments[field.id]?.lowercase()) {
|
||||
"center" -> TextAlign.Center
|
||||
"end", "right" -> TextAlign.End
|
||||
else -> TextAlign.Start
|
||||
}
|
||||
val alignment =
|
||||
when (fieldAlignments[field.id]?.lowercase()) {
|
||||
"center" -> Alignment.Center
|
||||
"end", "right" -> Alignment.CenterEnd
|
||||
else -> Alignment.CenterStart
|
||||
}
|
||||
val textAlign =
|
||||
when (fieldAlignments[field.id]?.lowercase()) {
|
||||
"center" -> TextAlign.Center
|
||||
"end", "right" -> TextAlign.End
|
||||
else -> TextAlign.Start
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
@@ -989,8 +1005,7 @@ fun ItemRow(
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
.padding(8.dp),
|
||||
).padding(8.dp),
|
||||
) {
|
||||
when (field.getType()) {
|
||||
com.collabtable.app.data.model.FieldType.TEXT -> {
|
||||
@@ -1330,8 +1345,7 @@ fun ItemRow(
|
||||
MaterialTheme.colorScheme.surface
|
||||
},
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
)
|
||||
.border(
|
||||
).border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.outline,
|
||||
RoundedCornerShape(4.dp),
|
||||
@@ -1349,15 +1363,15 @@ fun ItemRow(
|
||||
}
|
||||
|
||||
com.collabtable.app.data.model.FieldType.LOCATION -> {
|
||||
Text(
|
||||
text = value?.value ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = textAlign,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
)
|
||||
Text(
|
||||
text = value?.value ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = textAlign,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
com.collabtable.app.data.model.FieldType.IMAGE -> {
|
||||
@@ -1469,26 +1483,26 @@ private fun EqualHeightRow(
|
||||
val count = measurables.size
|
||||
val n = widthsPx.size.coerceAtMost(count)
|
||||
|
||||
// First pass: measure children with fixed widths and unconstrained height to find max height
|
||||
var maxHeight = 0
|
||||
val placeablesFirst = Array(n) { i ->
|
||||
// Compute required row height using intrinsics to avoid measuring the same child twice
|
||||
var rowHeight = outerConstraints.minHeight
|
||||
for (i in 0 until n) {
|
||||
val w = widthsPx[i].coerceAtLeast(0)
|
||||
val c = Constraints.fixedWidth(w)
|
||||
val p = measurables[i].measure(c)
|
||||
if (p.height > maxHeight) maxHeight = p.height
|
||||
p
|
||||
val h = measurables[i].maxIntrinsicHeight(w)
|
||||
if (h > rowHeight) rowHeight = h
|
||||
}
|
||||
rowHeight = rowHeight.coerceIn(outerConstraints.minHeight, outerConstraints.maxHeight)
|
||||
|
||||
// Second pass: remeasure with fixed width and fixed row height so cells share same height
|
||||
val placeables = Array(n) { i ->
|
||||
val w = widthsPx[i].coerceAtLeast(0)
|
||||
val c = Constraints.fixed(width = w, height = maxHeight)
|
||||
measurables[i].measure(c)
|
||||
}
|
||||
// Measure once with fixed width and the computed row height so all cells match
|
||||
val placeables =
|
||||
Array(n) { i ->
|
||||
val w = widthsPx[i].coerceAtLeast(0)
|
||||
val c = Constraints.fixed(width = w, height = rowHeight)
|
||||
measurables[i].measure(c)
|
||||
}
|
||||
|
||||
val totalWidth = widthsPx.take(n).sum()
|
||||
val layoutWidth = totalWidth.coerceIn(outerConstraints.minWidth, outerConstraints.maxWidth)
|
||||
val layoutHeight = maxHeight.coerceIn(outerConstraints.minHeight, outerConstraints.maxHeight)
|
||||
val totalWidth = widthsPx.take(n).sum()
|
||||
val layoutWidth = totalWidth.coerceIn(outerConstraints.minWidth, outerConstraints.maxWidth)
|
||||
val layoutHeight = rowHeight
|
||||
|
||||
layout(layoutWidth, layoutHeight) {
|
||||
var x = 0
|
||||
@@ -1785,12 +1799,14 @@ fun AddFieldDialog(
|
||||
when (normalizedType) {
|
||||
"CURRENCY" -> currency.trim()
|
||||
"DROPDOWN" ->
|
||||
dropdownOptions.split(",")
|
||||
dropdownOptions
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString("|")
|
||||
"AUTOCOMPLETE" ->
|
||||
dropdownOptions.split(",")
|
||||
dropdownOptions
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString("|")
|
||||
@@ -2126,12 +2142,14 @@ fun EditFieldDialog(
|
||||
when (normalizedType) {
|
||||
"CURRENCY", "PRICE" -> currency.trim()
|
||||
"DROPDOWN" ->
|
||||
dropdownOptions.split(",")
|
||||
dropdownOptions
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString("|")
|
||||
"AUTOCOMPLETE" ->
|
||||
dropdownOptions.split(",")
|
||||
dropdownOptions
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString("|")
|
||||
@@ -2141,11 +2159,12 @@ fun EditFieldDialog(
|
||||
onUpdate(name.trim(), normalizedType, options)
|
||||
// Persist alignment selection for this field
|
||||
val current = prefs.getColumnAlignments(field.listId).toMutableMap()
|
||||
current[field.id] = when (selectedAlignment.lowercase()) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
current[field.id] =
|
||||
when (selectedAlignment.lowercase()) {
|
||||
"center" -> "center"
|
||||
"end", "right" -> "end"
|
||||
else -> "start"
|
||||
}
|
||||
prefs.setColumnAlignments(field.listId, current)
|
||||
},
|
||||
enabled = name.isNotBlank() && (selectedFieldType !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()),
|
||||
@@ -2729,13 +2748,13 @@ fun FieldInput(
|
||||
.background(
|
||||
color =
|
||||
try {
|
||||
androidx.compose.ui.graphics.Color(android.graphics.Color.parseColor(value))
|
||||
androidx.compose.ui.graphics
|
||||
.Color(android.graphics.Color.parseColor(value))
|
||||
} catch (e: Exception) {
|
||||
MaterialTheme.colorScheme.surface
|
||||
},
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
)
|
||||
.border(
|
||||
).border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.outline,
|
||||
RoundedCornerShape(4.dp),
|
||||
|
||||
+11
-4
@@ -43,7 +43,9 @@ class ListDetailViewModel(
|
||||
|
||||
private fun loadListData() {
|
||||
viewModelScope.launch {
|
||||
database.listDao().getListWithFields(listId)
|
||||
database
|
||||
.listDao()
|
||||
.getListWithFields(listId)
|
||||
.debounce(75)
|
||||
.collect { listWithFields ->
|
||||
_list.value = listWithFields?.list
|
||||
@@ -51,7 +53,8 @@ class ListDetailViewModel(
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
database.itemDao()
|
||||
database
|
||||
.itemDao()
|
||||
.getItemsWithValuesForList(listId)
|
||||
.debounce(75)
|
||||
.collect { itemsData ->
|
||||
@@ -60,7 +63,9 @@ class ListDetailViewModel(
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
database.fieldDao().getFieldsForList(listId)
|
||||
database
|
||||
.fieldDao()
|
||||
.getFieldsForList(listId)
|
||||
.debounce(75)
|
||||
.collect { fieldsData ->
|
||||
_fields.value = fieldsData
|
||||
@@ -70,7 +75,9 @@ class ListDetailViewModel(
|
||||
|
||||
private fun startPeriodicSync() {
|
||||
viewModelScope.launch {
|
||||
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
|
||||
val prefs =
|
||||
com.collabtable.app.data.preferences.PreferencesManager
|
||||
.getInstance(context)
|
||||
while (true) {
|
||||
performSync()
|
||||
val delayMs = prefs.getSyncPollIntervalMs()
|
||||
|
||||
+15
-5
@@ -42,7 +42,9 @@ class ListsViewModel(
|
||||
|
||||
private fun loadLists() {
|
||||
viewModelScope.launch {
|
||||
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
|
||||
val prefs =
|
||||
com.collabtable.app.data.preferences.PreferencesManager
|
||||
.getInstance(context)
|
||||
val listsFlow = database.listDao().getAllLists()
|
||||
val sortFlow = prefs.sortOrder
|
||||
|
||||
@@ -102,7 +104,9 @@ class ListsViewModel(
|
||||
}
|
||||
|
||||
private suspend fun startPeriodicSync() {
|
||||
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
|
||||
val prefs =
|
||||
com.collabtable.app.data.preferences.PreferencesManager
|
||||
.getInstance(context)
|
||||
while (true) {
|
||||
val delayMs = prefs.getSyncPollIntervalMs()
|
||||
kotlinx.coroutines.delay(delayMs)
|
||||
@@ -189,21 +193,27 @@ class ListsViewModel(
|
||||
}
|
||||
|
||||
private fun maybeNotifyListAdded(list: CollabList) {
|
||||
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
|
||||
val prefs =
|
||||
com.collabtable.app.data.preferences.PreferencesManager
|
||||
.getInstance(context)
|
||||
if (prefs.notifyListAdded.value && !isInForeground()) {
|
||||
NotificationHelper.showListAdded(context, list.id, list.name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeNotifyListEdited(list: CollabList) {
|
||||
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
|
||||
val prefs =
|
||||
com.collabtable.app.data.preferences.PreferencesManager
|
||||
.getInstance(context)
|
||||
if (prefs.notifyListEdited.value && !isInForeground()) {
|
||||
NotificationHelper.showListEdited(context, list.id, list.name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeNotifyListRemoved(list: CollabList) {
|
||||
val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context)
|
||||
val prefs =
|
||||
com.collabtable.app.data.preferences.PreferencesManager
|
||||
.getInstance(context)
|
||||
if (prefs.notifyListRemoved.value && !isInForeground()) {
|
||||
NotificationHelper.showListRemoved(context, list.id, list.name)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
@@ -56,7 +56,10 @@ import com.collabtable.app.utils.LogEntry
|
||||
import com.collabtable.app.utils.LogLevel
|
||||
import com.collabtable.app.utils.Logger
|
||||
|
||||
enum class TimeRange(val label: String, val milliseconds: Long) {
|
||||
enum class TimeRange(
|
||||
val label: String,
|
||||
val milliseconds: Long,
|
||||
) {
|
||||
LAST_10_SECONDS("Last 10 seconds", 10_000),
|
||||
LAST_30_SECONDS("Last 30 seconds", 30_000),
|
||||
LAST_MINUTE("Last minute", 60_000),
|
||||
@@ -146,19 +149,29 @@ fun LogsScreen(onNavigateBack: () -> Unit) {
|
||||
// Export / Share current (filtered) logs
|
||||
IconButton(onClick = {
|
||||
if (filteredLogs.isEmpty()) return@IconButton
|
||||
val exportText = buildString {
|
||||
append("CollabTable Logs Export\n")
|
||||
append("Total: ").append(filteredLogs.size).append('\n')
|
||||
append("Generated: ").append(java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())).append("\n\n")
|
||||
filteredLogs.forEach { log ->
|
||||
append(log.toFormattedString()).append('\n')
|
||||
val exportText =
|
||||
buildString {
|
||||
append("CollabTable Logs Export\n")
|
||||
append("Total: ").append(filteredLogs.size).append('\n')
|
||||
append("Generated: ")
|
||||
append(
|
||||
java.text
|
||||
.SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
java.util.Locale.getDefault(),
|
||||
).format(java.util.Date()),
|
||||
)
|
||||
append("\n\n")
|
||||
filteredLogs.forEach { log ->
|
||||
append(log.toFormattedString()).append('\n')
|
||||
}
|
||||
}
|
||||
val intent =
|
||||
android.content.Intent(android.content.Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(android.content.Intent.EXTRA_TEXT, exportText)
|
||||
putExtra(android.content.Intent.EXTRA_TITLE, "CollabTable Logs")
|
||||
}
|
||||
}
|
||||
val intent = android.content.Intent(android.content.Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(android.content.Intent.EXTRA_TEXT, exportText)
|
||||
putExtra(android.content.Intent.EXTRA_TITLE, "CollabTable Logs")
|
||||
}
|
||||
val chooser = android.content.Intent.createChooser(intent, "Share Logs")
|
||||
try {
|
||||
context.startActivity(chooser)
|
||||
@@ -293,12 +306,11 @@ fun LogsScreen(onNavigateBack: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasActiveFilters(filters: LogFilters): Boolean {
|
||||
return filters.severities.size < LogLevel.values().size ||
|
||||
private fun hasActiveFilters(filters: LogFilters): Boolean =
|
||||
filters.severities.size < LogLevel.values().size ||
|
||||
filters.timeRange != TimeRange.ALL_TIME ||
|
||||
filters.tags.isNotEmpty() ||
|
||||
filters.searchText.isNotEmpty()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user