From a7fff59f8c19c010f8de4dbf1be10ff0869492fe Mon Sep 17 00:00:00 2001 From: gabriel20xx Date: Tue, 11 Nov 2025 23:18:50 +0100 Subject: [PATCH] feat: enhance code readability by applying consistent formatting and style adjustments across multiple files --- CollabTableAndroid/app/build.gradle | 2 + .../data/preferences/PreferencesManager.kt | 85 +++---- .../app/data/repository/SyncRepository.kt | 26 ++- .../app/notifications/NotificationHelper.kt | 9 +- .../collabtable/app/ui/navigation/MainApp.kt | 8 +- .../app/ui/screens/ListDetailScreen.kt | 221 ++++++++++-------- .../app/ui/screens/ListDetailViewModel.kt | 15 +- .../app/ui/screens/ListsViewModel.kt | 20 +- .../collabtable/app/ui/screens/LogsScreen.kt | 46 ++-- CollabTableAndroid/detekt.yml | 70 ++++++ 10 files changed, 309 insertions(+), 193 deletions(-) diff --git a/CollabTableAndroid/app/build.gradle b/CollabTableAndroid/app/build.gradle index f7b789c..4d2adab 100644 --- a/CollabTableAndroid/app/build.gradle +++ b/CollabTableAndroid/app/build.gradle @@ -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 diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt index 5f86a35..709b7f2 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt @@ -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 = _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) { + fun setColumnAlignments( + listId: String, + alignments: Map, + ) { 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 } } - } } } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt index 58899b0..ccdf1cd 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt @@ -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 } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/notifications/NotificationHelper.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/notifications/NotificationHelper.kt index 1edecef..91c4088 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/notifications/NotificationHelper.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/notifications/NotificationHelper.kt @@ -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") diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/navigation/MainApp.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/navigation/MainApp.kt index 8cbd659..f3061ea 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/navigation/MainApp.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/navigation/MainApp.kt @@ -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 diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt index db2d475..c3c7314 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt @@ -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>, ) -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), diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt index 1fa2bde..6fe420c 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt @@ -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() diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt index 63bb45c..097ad41 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt @@ -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) } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt index f52d278..5af7f11 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/LogsScreen.kt @@ -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 diff --git a/CollabTableAndroid/detekt.yml b/CollabTableAndroid/detekt.yml index e69de29..19012ba 100644 --- a/CollabTableAndroid/detekt.yml +++ b/CollabTableAndroid/detekt.yml @@ -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