feat: enhance code readability by applying consistent formatting and style adjustments across multiple files

This commit is contained in:
2025-11-11 23:18:50 +01:00
parent 7e4896fc69
commit a7fff59f8c
10 changed files with 309 additions and 193 deletions
+2
View File
@@ -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
@@ -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 }
}
}
}
}
@@ -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
}
@@ -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
@@ -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),
@@ -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()
@@ -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
+70
View File
@@ -0,0 +1,70 @@
config:
validation: true
warningsAsErrors: false
processors:
active: true
build:
maxIssues: 400 # allow current backlog without failing build
excludeCorrectable: false
comments:
active: true
style:
MagicNumber:
active: false # disabled until numeric constants consolidated
WildcardImport:
active: false
excludes: [ '**/test/**', '**/androidTest/**' ]
MaxLineLength:
active: true
maxLineLength: 200
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: true
complexity:
LongMethod:
active: true
threshold: 600
ignoreAnnotated: [ 'Composable' ]
CyclomaticComplexMethod:
active: true
threshold: 80
ignoreAnnotated: [ 'Composable' ]
LongParameterList:
active: true
functionThreshold: 16
constructorThreshold: 10
ignoreDefaultParameters: true
ignoreAnnotated: [ 'Composable' ]
TooManyFunctions:
active: true
thresholdInClasses: 80
thresholdInInterfaces: 50
thresholdInObjects: 50
thresholdInFiles: 90
exceptions:
TooGenericExceptionCaught:
active: true
exceptionNames:
- 'java.lang.Exception'
- 'java.lang.RuntimeException'
- 'java.lang.Throwable'
allowedExceptionNameRegex: '_|(ignore|expected)'
ignoreAnnotated: [ 'Composable' ]
SwallowedException:
active: true # enable to surface real issues
empty-blocks:
active: true
performance:
active: true
naming:
FunctionNaming:
active: false