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