feat: enhance code readability by applying consistent formatting and style adjustments across multiple files
This commit is contained in:
@@ -108,6 +108,8 @@ ktlint {
|
|||||||
detekt {
|
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
|
||||||
|
|||||||
+26
-41
@@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
+18
-8
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -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
|
||||||
|
|||||||
+63
-44
@@ -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),
|
||||||
|
|||||||
+11
-4
@@ -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()
|
||||||
|
|||||||
+15
-5
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user