diff --git a/CollabTableAndroid/app/detekt-baseline.xml b/CollabTableAndroid/app/detekt-baseline.xml
new file mode 100644
index 0000000..8a90cd1
--- /dev/null
+++ b/CollabTableAndroid/app/detekt-baseline.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ ComplexCondition:Logger.kt$Logger$last != null && last.level == level && last.tag == tag && last.message == message && (now - last.timestamp) <= DEDUPE_WINDOW_MS
+ ExplicitItLambdaParameter:ListsScreen.kt${ _, it -> it.id }
+ ReturnCount:ListsViewModel.kt$ListsViewModel$suspend fun exportListToCsv( listId: String, listName: String, targetTreeUri: Uri? = null, ): Result<String>
+ ReturnCount:SyncRepository.kt$SyncRepository$private fun isNetworkAvailable(): Boolean
+ SwallowedException:ApiClient.kt$ApiClient$e: Exception
+ SwallowedException:ConnectionStatus.kt$e: Exception
+ SwallowedException:Field.kt$Field$e: Exception
+ SwallowedException:ListChangeNotificationWorker.kt$ListChangeNotificationWorker$e: Exception
+ SwallowedException:ListDetailScreen.kt$e: Exception
+ SwallowedException:PreferencesManager.kt$PreferencesManager$e: Exception
+ SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: ConnectException
+ SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: Exception
+ SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: NetworkOnMainThreadException
+ SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: SocketTimeoutException
+ SwallowedException:ServerSetupViewModel.kt$ServerSetupViewModel$e: UnknownHostException
+ UnusedParameter:ListDetailScreen.kt$onAlignmentChange: (String) -> Unit = {}
+ UnusedParameter:ListDetailScreen.kt$onUpdateAlignment: (String, String) -> Unit
+ UnusedParameter:ListsScreen.kt$onNavigateToLogs: () -> Unit = {}
+ UnusedParameter:ListsScreen.kt$onNavigateToSettings: () -> Unit
+ UnusedParameter:SettingsScreen.kt$onNavigateBack: () -> Unit
+ UnusedPrivateMember:ListDetailScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun FilterSortDialog( fields: List<Field>, currentSortField: Field?, currentSortAscending: Boolean, currentGroupByField: Field?, currentFilterField: Field?, currentFilterValue: String, onDismiss: () -> Unit, onApply: (sortField: Field?, sortAscending: Boolean, groupByField: Field?, filterField: Field?, filterValue: String) -> Unit, onClearAll: () -> Unit, )
+ UnusedPrivateMember:ListDetailViewModel.kt$ListDetailViewModel$private fun isInForeground(): Boolean
+
+
diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/CollabTableApplication.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/CollabTableApplication.kt
index 9e9b48a..8c5490d 100644
--- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/CollabTableApplication.kt
+++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/CollabTableApplication.kt
@@ -53,6 +53,7 @@ class CollabTableApplication : Application() {
NotificationHelper.clearListEventNotifications(this@CollabTableApplication)
NotificationPoller.start(this@CollabTableApplication)
}
+
override fun onStop(owner: LifecycleOwner) {
NotificationPoller.stop()
}
diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/api/CollabTableApi.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/api/CollabTableApi.kt
index 1012e1c..d0100ad 100644
--- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/api/CollabTableApi.kt
+++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/api/CollabTableApi.kt
@@ -6,8 +6,8 @@ import com.collabtable.app.data.model.Item
import com.collabtable.app.data.model.ItemValue
import retrofit2.Response
import retrofit2.http.Body
-import retrofit2.http.POST
import retrofit2.http.GET
+import retrofit2.http.POST
import retrofit2.http.Query
data class SyncRequest(
diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt
index c338193..0d06abc 100644
--- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt
+++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt
@@ -48,7 +48,11 @@ interface FieldDao {
suspend fun getFieldsUpdatedSince(since: Long): List
@Query("UPDATE fields SET `order` = :newOrder, updatedAt = :timestamp WHERE id = :fieldId")
- suspend fun updateFieldOrder(fieldId: String, newOrder: Int, timestamp: Long)
+ suspend fun updateFieldOrder(
+ fieldId: String,
+ newOrder: Int,
+ timestamp: Long,
+ )
// Reorder fields in a single transaction for clarity and consistency
@Transaction
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 725f455..14a626a 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
@@ -16,8 +16,8 @@ class PreferencesManager(
private val _serverUrl = MutableStateFlow(getServerUrl())
val serverUrl: StateFlow = _serverUrl.asStateFlow()
- private val _isFirstRun = MutableStateFlow(isFirstRun())
- val isFirstRunFlow: StateFlow = _isFirstRun.asStateFlow()
+ private val _isFirstRunFlow = MutableStateFlow(isFirstRun())
+ val isFirstRunFlow: StateFlow = _isFirstRunFlow.asStateFlow()
private val _themeMode = MutableStateFlow(getThemeMode())
val themeMode: StateFlow = _themeMode.asStateFlow()
@@ -67,7 +67,7 @@ class PreferencesManager(
fun setIsFirstRun(isFirstRun: Boolean) {
prefs.edit().putBoolean(KEY_FIRST_RUN, isFirstRun).apply()
- _isFirstRun.value = isFirstRun
+ _isFirstRunFlow.value = isFirstRun
}
fun getServerPassword(): String? = prefs.getString(KEY_SERVER_PASSWORD, null)
@@ -272,7 +272,10 @@ class PreferencesManager(
fun getDeviceId(): String {
val existing = prefs.getString(KEY_DEVICE_ID, null)
if (!existing.isNullOrBlank()) return existing
- val id = java.util.UUID.randomUUID().toString()
+ val id =
+ java.util.UUID
+ .randomUUID()
+ .toString()
prefs.edit().putString(KEY_DEVICE_ID, id).apply()
return id
}
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 c8070aa..14351ee 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
@@ -1,8 +1,12 @@
package com.collabtable.app.notifications
+import android.Manifest
import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
import com.collabtable.app.R
object NotificationHelper {
@@ -34,6 +38,15 @@ object NotificationHelper {
id: Int,
builder: NotificationCompat.Builder,
) {
+ if (
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS,
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ return
+ }
with(NotificationManagerCompat.from(context)) {
notify(id, builder.build())
}
diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/notifications/NotificationPoller.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/notifications/NotificationPoller.kt
index b58ce57..daffcc8 100644
--- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/notifications/NotificationPoller.kt
+++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/notifications/NotificationPoller.kt
@@ -1,95 +1,102 @@
-package com.collabtable.app.notifications
-
-import android.content.Context
-import com.collabtable.app.data.api.ApiClient
-import com.collabtable.app.data.api.CollabTableApi
-import com.collabtable.app.data.api.NotificationEvent
-import com.collabtable.app.data.database.CollabTableDatabase
-import com.collabtable.app.data.preferences.PreferencesManager
-import com.collabtable.app.utils.Logger
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
-
-object NotificationPoller {
- @Volatile private var job: Job? = null
-
- fun start(context: Context) {
- if (job?.isActive == true) return
- val appCtx = context.applicationContext
- val prefs = PreferencesManager.getInstance(appCtx)
- val api = ApiClient.api
- val db = CollabTableDatabase.getDatabase(appCtx)
- job = CoroutineScope(Dispatchers.IO).launch {
- while (isActive) {
- try {
- // Skip if password missing (auth disabled)
- val pwd = prefs.getServerPassword()?.trim()
- if (pwd.isNullOrBlank() || pwd == "\$password") {
- delay(2_000)
- continue
- }
- val since = prefs.getLastListNotifyCheckTimestamp()
- val resp = api.pollNotifications(since)
- if (resp.isSuccessful) {
- val body = resp.body()
- val events = body?.notifications.orEmpty()
- if (events.isNotEmpty()) {
- // Map and emit based on preferences
- events.forEach { ev ->
- val listId = ev.listId ?: return@forEach
- val list = db.listDao().getListById(listId) ?: return@forEach
- handleEvent(appCtx, ev, list.name, prefs)
- }
- }
- // Advance checkpoint to server timestamp to avoid re-processing
- val stamp = body?.serverTimestamp ?: System.currentTimeMillis()
- prefs.setLastListNotifyCheckTimestamp(stamp)
- } else if (resp.code() == 401) {
- // Unauthorized; back off by advancing timestamp minimally to avoid tight loop
- delay(5_000)
- }
- } catch (e: Exception) {
- try { Logger.w("NotifPoll", "Polling failed: ${e.message}") } catch (_: Exception) {}
- // brief backoff
- delay(1_500)
- }
- val interval = prefs.syncPollIntervalMs.value
- delay(interval)
- }
- }
- }
-
- fun stop() {
- job?.cancel()
- job = null
- }
-
- private fun handleEvent(
- context: Context,
- ev: NotificationEvent,
- listName: String,
- prefs: PreferencesManager,
- ) {
- // Respect notification switches by grouping semantics
- when (ev.entityType.lowercase()) {
- "list" -> when (ev.eventType.lowercase()) {
- "created" -> if (prefs.notifyListAdded.value) NotificationHelper.showListAdded(context, ev.listId ?: "", listName)
- "updated" -> if (prefs.notifyListEdited.value) NotificationHelper.showListEdited(context, ev.listId ?: "", listName)
- "deleted" -> if (prefs.notifyListRemoved.value) NotificationHelper.showListRemoved(context, ev.listId ?: "", listName)
- }
- // Treat fields/items/value changes as content updates
- "field", "item", "value" -> if (prefs.notifyListContentUpdated.value) {
- NotificationHelper.showListContentUpdated(context, ev.listId ?: "", listName)
- }
- else -> when (ev.eventType.lowercase()) {
- "listcontentupdated" -> if (prefs.notifyListContentUpdated.value) {
- NotificationHelper.showListContentUpdated(context, ev.listId ?: "", listName)
- }
- }
- }
- }
-}
+package com.collabtable.app.notifications
+
+import android.content.Context
+import com.collabtable.app.data.api.ApiClient
+import com.collabtable.app.data.api.NotificationEvent
+import com.collabtable.app.data.database.CollabTableDatabase
+import com.collabtable.app.data.preferences.PreferencesManager
+import com.collabtable.app.utils.Logger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+
+object NotificationPoller {
+ @Volatile private var job: Job? = null
+
+ fun start(context: Context) {
+ if (job?.isActive == true) return
+ val appCtx = context.applicationContext
+ val prefs = PreferencesManager.getInstance(appCtx)
+ val api = ApiClient.api
+ val db = CollabTableDatabase.getDatabase(appCtx)
+ job =
+ CoroutineScope(Dispatchers.IO).launch {
+ while (isActive) {
+ try {
+ // Skip if password missing (auth disabled)
+ val pwd = prefs.getServerPassword()?.trim()
+ if (pwd.isNullOrBlank() || pwd == "\$password") {
+ delay(2_000)
+ continue
+ }
+ val since = prefs.getLastListNotifyCheckTimestamp()
+ val resp = api.pollNotifications(since)
+ if (resp.isSuccessful) {
+ val body = resp.body()
+ val events = body?.notifications.orEmpty()
+ if (events.isNotEmpty()) {
+ // Map and emit based on preferences
+ events.forEach { ev ->
+ val listId = ev.listId ?: return@forEach
+ val list = db.listDao().getListById(listId) ?: return@forEach
+ handleEvent(appCtx, ev, list.name, prefs)
+ }
+ }
+ // Advance checkpoint to server timestamp to avoid re-processing
+ val stamp = body?.serverTimestamp ?: System.currentTimeMillis()
+ prefs.setLastListNotifyCheckTimestamp(stamp)
+ } else if (resp.code() == 401) {
+ // Unauthorized; back off by advancing timestamp minimally to avoid tight loop
+ delay(5_000)
+ }
+ } catch (e: Exception) {
+ try {
+ Logger.w("NotifPoll", "Polling failed: ${e.message}")
+ } catch (_: Exception) {
+ }
+ // brief backoff
+ delay(1_500)
+ }
+ val interval = prefs.syncPollIntervalMs.value
+ delay(interval)
+ }
+ }
+ }
+
+ fun stop() {
+ job?.cancel()
+ job = null
+ }
+
+ private fun handleEvent(
+ context: Context,
+ ev: NotificationEvent,
+ listName: String,
+ prefs: PreferencesManager,
+ ) {
+ // Respect notification switches by grouping semantics
+ when (ev.entityType.lowercase()) {
+ "list" ->
+ when (ev.eventType.lowercase()) {
+ "created" -> if (prefs.notifyListAdded.value) NotificationHelper.showListAdded(context, ev.listId ?: "", listName)
+ "updated" -> if (prefs.notifyListEdited.value) NotificationHelper.showListEdited(context, ev.listId ?: "", listName)
+ "deleted" -> if (prefs.notifyListRemoved.value) NotificationHelper.showListRemoved(context, ev.listId ?: "", listName)
+ }
+ // Treat fields/items/value changes as content updates
+ "field", "item", "value" ->
+ if (prefs.notifyListContentUpdated.value) {
+ NotificationHelper.showListContentUpdated(context, ev.listId ?: "", listName)
+ }
+ else ->
+ when (ev.eventType.lowercase()) {
+ "listcontentupdated" ->
+ if (prefs.notifyListContentUpdated.value) {
+ NotificationHelper.showListContentUpdated(context, ev.listId ?: "", listName)
+ }
+ }
+ }
+ }
+}
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 75d2036..c15af1a 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
@@ -51,7 +51,6 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
@@ -226,6 +225,7 @@ fun ListDetailScreen(
floatingActionButton = {},
) { padding ->
// Apply filtering, sorting and grouping off the main thread and memoize the result
+ @Suppress("ProduceStateDoesNotAssignValue")
val transformed by produceState(
initialValue = TransformedItems(emptyList(), mapOf("_all" to emptyList())),
stableItems,
@@ -235,7 +235,7 @@ fun ListDetailScreen(
filterField,
filterValue,
) {
- value =
+ val transformedItems =
withContext(Dispatchers.Default) {
transformItems(
items = stableItems,
@@ -246,6 +246,7 @@ fun ListDetailScreen(
filterValue = filterValue,
)
}
+ value = transformedItems
}
val processedItems = transformed.processed
@@ -550,17 +551,19 @@ fun ListDetailScreen(
}
stableItems.isEmpty() -> {
Box(
- modifier = Modifier
- .fillMaxSize(),
+ modifier =
+ Modifier
+ .fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(R.string.no_items),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp),
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp),
textAlign = TextAlign.Center,
)
}
@@ -1046,8 +1049,7 @@ fun ItemRow(
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outline,
- )
- .fillMaxHeight()
+ ).fillMaxHeight()
// Consistent minimum top/bottom padding regardless of row height
.padding(horizontal = 8.dp, vertical = 8.dp),
// Center horizontally based on alignment while always vertically centering
@@ -1105,7 +1107,8 @@ fun ItemRow(
}
com.collabtable.app.data.model.FieldType.SELECT,
- com.collabtable.app.data.model.FieldType.MULTI_SELECT -> {
+ com.collabtable.app.data.model.FieldType.MULTI_SELECT,
+ -> {
Text(
text = value?.value ?: "",
style = MaterialTheme.typography.bodyMedium,
@@ -2365,9 +2368,10 @@ fun FieldInput(
com.collabtable.app.data.model.FieldType.MULTI_SELECT -> {
val options = field.getSelectOptions()
var expanded by remember { mutableStateOf(false) }
- val selectedOptions = remember(value) {
- value.split(", ").filter { it.isNotBlank() }.toSet()
- }
+ val selectedOptions =
+ remember(value) {
+ value.split(", ").filter { it.isNotBlank() }.toSet()
+ }
ExposedDropdownMenuBox(
expanded = expanded,
@@ -2394,19 +2398,20 @@ fun FieldInput(
options.forEach { option ->
val isSelected = selectedOptions.contains(option)
DropdownMenuItem(
- text = {
+ text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = isSelected, onCheckedChange = null)
Spacer(modifier = Modifier.width(8.dp))
- Text(option)
+ Text(option)
}
},
onClick = {
- val newSelected = if (isSelected) {
- selectedOptions - option
- } else {
- selectedOptions + option
- }
+ val newSelected =
+ if (isSelected) {
+ selectedOptions - option
+ } else {
+ selectedOptions + option
+ }
val orderedNewSelected = options.filter { it in newSelected }
onValueChange(orderedNewSelected.joinToString(", "))
},
@@ -4083,11 +4088,3 @@ private fun RenameListDialog(
},
)
}
-
-
-
-
-
-
-
-
diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt
index e4d37a5..9dd098e 100644
--- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt
+++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt
@@ -3,7 +3,9 @@
package com.collabtable.app.ui.screens
import android.Manifest
+import android.content.Intent
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
@@ -29,9 +31,6 @@ import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Share
-import android.net.Uri
-import android.content.Intent
-import androidx.documentfile.provider.DocumentFile
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
@@ -60,6 +59,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
+import androidx.documentfile.provider.DocumentFile
import com.collabtable.app.R
import com.collabtable.app.data.database.CollabTableDatabase
import com.collabtable.app.data.model.CollabList
@@ -349,32 +349,39 @@ fun ListsScreen(
}
// Export dialog (top-level) separate from create/edit/delete dialogs
listToExport?.let { list ->
- val folderPicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
- if (uri != null) {
- // Take persistable permission for potential reuse during this session
- try {
- val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- context.contentResolver.takePersistableUriPermission(uri, flags)
- } catch (_: Exception) { }
- exportTargetUri = uri
+ val folderPicker =
+ rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
+ if (uri != null) {
+ // Take persistable permission for potential reuse during this session
+ try {
+ val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ context.contentResolver.takePersistableUriPermission(uri, flags)
+ } catch (_: Exception) {
+ }
+ exportTargetUri = uri
+ }
}
- }
ExportListDialog(
list = list,
- selectedDirectoryLabel = exportTargetUri?.let { uri ->
- DocumentFile.fromTreeUri(context, uri)?.name ?: uri.lastPathSegment ?: "(selected)"
- } ?: "App storage (default)",
+ selectedDirectoryLabel =
+ exportTargetUri?.let { uri ->
+ DocumentFile.fromTreeUri(context, uri)?.name ?: uri.lastPathSegment ?: "(selected)"
+ } ?: "App storage (default)",
onSelectDirectory = { folderPicker.launch(null) },
- onDismiss = { listToExport = null; exportTargetUri = null },
+ onDismiss = {
+ listToExport = null
+ exportTargetUri = null
+ },
onConfirmExport = { format ->
if (format == "CSV") {
coroutineScope.launch {
val result = viewModel.exportListToCsv(list.id, list.name, exportTargetUri)
- result.onSuccess { path ->
- Toast.makeText(context, context.getString(R.string.export_success, path), Toast.LENGTH_LONG).show()
- }.onFailure {
- Toast.makeText(context, context.getString(R.string.export_error), Toast.LENGTH_LONG).show()
- }
+ result
+ .onSuccess { path ->
+ Toast.makeText(context, context.getString(R.string.export_success, path), Toast.LENGTH_LONG).show()
+ }.onFailure {
+ Toast.makeText(context, context.getString(R.string.export_error), Toast.LENGTH_LONG).show()
+ }
listToExport = null
exportTargetUri = null
}
@@ -500,7 +507,6 @@ fun CreateListDialog(
}
},
)
-
}
@Composable
@@ -612,7 +618,10 @@ private fun ExportListDialog(
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(
text = { Text(stringResource(R.string.format_csv)) },
- onClick = { selectedFormat = "CSV"; expanded = false },
+ onClick = {
+ selectedFormat = "CSV"
+ expanded = false
+ },
)
}
}
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 26e3b47..82e251c 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
@@ -1,6 +1,8 @@
package com.collabtable.app.ui.screens
import android.content.Context
+import android.net.Uri
+import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.ViewModel
@@ -19,12 +21,10 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.io.File
-import android.net.Uri
-import androidx.documentfile.provider.DocumentFile
import java.text.SimpleDateFormat
import java.util.Date
-import java.util.UUID
import java.util.Locale
+import java.util.UUID
class ListsViewModel(
private val database: CollabTableDatabase,
@@ -239,34 +239,41 @@ class ListsViewModel(
targetTreeUri: Uri? = null,
): Result {
return try {
- // Load fields (ordered) and items with values
- val fields: List = database.fieldDao().getFieldsForList(listId).first()
- val items: List = database.itemDao().getItemsWithValuesForList(listId).first()
+ // Load fields (ordered) and items with values
+ val fields: List = database.fieldDao().getFieldsForList(listId).first()
+ val items: List = database.itemDao().getItemsWithValuesForList(listId).first()
- // Build header
- val headers = fields.map { escapeCsv(it.name) }
- val fieldIdOrder = fields.map { it.id }
+ // Build header
+ val headers = fields.map { escapeCsv(it.name) }
+ val fieldIdOrder = fields.map { it.id }
- val rows = items.map { iwv ->
- val valueMap = iwv.values.associateBy { it.fieldId }
- fieldIdOrder.joinToString(",") { fid ->
- val raw = valueMap[fid]?.value ?: ""
- escapeCsv(raw)
- }
- }
+ val rows =
+ items.map { iwv ->
+ val valueMap = iwv.values.associateBy { it.fieldId }
+ fieldIdOrder.joinToString(",") { fid ->
+ val raw = valueMap[fid]?.value ?: ""
+ escapeCsv(raw)
+ }
+ }
- val csv = buildString {
- append(headers.joinToString(","))
- append('\n')
- rows.forEachIndexed { idx, row ->
- append(row)
- if (idx != rows.lastIndex) append('\n')
- }
- }
+ val csv =
+ buildString {
+ append(headers.joinToString(","))
+ append('\n')
+ rows.forEachIndexed { idx, row ->
+ append(row)
+ if (idx != rows.lastIndex) append('\n')
+ }
+ }
- val safeBase = listName.lowercase(Locale.US).replace("[^a-z0-9]+".toRegex(), "_").trim('_').ifBlank { "table" }
- val ts = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
- val fileName = "${safeBase}_${ts}.csv"
+ val safeBase =
+ listName
+ .lowercase(Locale.US)
+ .replace("[^a-z0-9]+".toRegex(), "_")
+ .trim('_')
+ .ifBlank { "table" }
+ val ts = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
+ val fileName = "${safeBase}_$ts.csv"
if (targetTreeUri != null) {
val docTree = DocumentFile.fromTreeUri(context, targetTreeUri)
if (docTree == null || !docTree.isDirectory || !docTree.canWrite()) {
diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt
index 3f2ad89..0465ea7 100644
--- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt
+++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt
@@ -453,7 +453,7 @@ fun SettingsScreen(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
- Text(
+ Text(
text = "View application logs",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -538,7 +538,8 @@ fun SettingsScreen(
onDismissRequest = { showLeaveDialog = false },
title = { Text(stringResource(R.string.leave_server_dialog_title)) },
text = {
- Text(stringResource(R.string.leave_server_dialog_body),
+ Text(
+ stringResource(R.string.leave_server_dialog_body),
)
},
confirmButton = {
diff --git a/CollabTableServer/src/index.ts b/CollabTableServer/src/index.ts
index 8f4a49d..e4989a0 100644
--- a/CollabTableServer/src/index.ts
+++ b/CollabTableServer/src/index.ts
@@ -26,7 +26,7 @@ function sleep(ms: number) {
async function initializeDatabaseWithRetry() {
let attempt = 0;
- while (true) {
+ for (;;) {
attempt += 1;
try {
diff --git a/CollabTableServer/src/routes/itemRoutes.ts b/CollabTableServer/src/routes/itemRoutes.ts
index c71b0b7..acf8c0c 100644
--- a/CollabTableServer/src/routes/itemRoutes.ts
+++ b/CollabTableServer/src/routes/itemRoutes.ts
@@ -57,7 +57,9 @@ router.post('/values', async (req: Request, res: Response) => {
if (listId) {
await enqueueNotification(dbAdapter, (req as any).deviceId, 'listContentUpdated', 'value', id, listId, Date.now());
}
- } catch {}
+ } catch (notificationError) {
+ void notificationError;
+ }
res.json(itemValue);
} catch (error) {
res.status(500).json({ error: 'Failed to save item value' });
@@ -101,7 +103,9 @@ router.delete('/:id', async (req: Request, res: Response) => {
const item = await dbAdapter.queryOne('SELECT * FROM items WHERE id = ?', [req.params.id]);
const listId = item ? ((item as any).listId ?? (item as any).listid) : undefined;
await enqueueNotification(dbAdapter, (req as any).deviceId, 'deleted', 'item', req.params.id, listId, updatedAt);
- } catch {}
+ } catch (notificationError) {
+ void notificationError;
+ }
res.json({ message: 'Item deleted successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to delete item' });
diff --git a/CollabTableServer/src/routes/syncRoutes.ts b/CollabTableServer/src/routes/syncRoutes.ts
index fcd0afb..a02d3a2 100644
--- a/CollabTableServer/src/routes/syncRoutes.ts
+++ b/CollabTableServer/src/routes/syncRoutes.ts
@@ -167,7 +167,9 @@ router.post('/sync', async (req: Request, res: Response) => {
try {
const ev = list.isDeleted ? 'deleted' : (safeLastSyncTimestamp === 0 ? 'created' : 'updated');
await enqueueNotification(tx, deviceId, ev, 'list', list.id, list.id, listTs.updatedAt);
- } catch {}
+ } catch (notificationError) {
+ void notificationError;
+ }
// If a list was deleted, cascade the deletion to child records on server
// - mark fields and items as deleted (tombstones) so other clients learn about them
// - remove item_values belonging to items under this list (no tombstone support for values)
@@ -201,7 +203,9 @@ router.post('/sync', async (req: Request, res: Response) => {
try {
const ev = field.isDeleted ? 'deleted' : (safeLastSyncTimestamp === 0 ? 'created' : 'updated');
await enqueueNotification(tx, deviceId, ev, 'field', field.id, field.listId, fieldTs.updatedAt);
- } catch {}
+ } catch (notificationError) {
+ void notificationError;
+ }
if (field.isDeleted) {
// Remove item_values for this field
try {
@@ -239,7 +243,9 @@ router.post('/sync', async (req: Request, res: Response) => {
try {
const ev = item.isDeleted ? 'deleted' : (safeLastSyncTimestamp === 0 ? 'created' : 'updated');
await enqueueNotification(tx, deviceId, ev, 'item', item.id, item.listId, itemTs.updatedAt);
- } catch {}
+ } catch (notificationError) {
+ void notificationError;
+ }
// If an item was deleted, remove its values (no tombstone support for values)
if (item.isDeleted) {
try {
@@ -324,7 +330,9 @@ router.post('/sync', async (req: Request, res: Response) => {
if (lId) {
await enqueueNotification(tx, deviceId, 'listContentUpdated', 'value', value.id, lId, valueUpdatedAt);
}
- } catch {}
+ } catch (notificationError) {
+ void notificationError;
+ }
} catch (err: any) {
// Catch FK violation just in case race or deletion happened inside same sync
if (err && err.code === '23503') {