feat: update dependencies and enhance sync logic for improved performance and reliability
This commit is contained in:
@@ -1,25 +1,26 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
id 'org.jetbrains.kotlin.plugin.compose'
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
id 'org.jlleitschuh.gradle.ktlint'
|
id 'org.jlleitschuh.gradle.ktlint'
|
||||||
id 'io.gitlab.arturbosch.detekt'
|
id 'io.gitlab.arturbosch.detekt'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.collabtable.app'
|
namespace = 'com.collabtable.app'
|
||||||
compileSdk 34
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.collabtable.app"
|
applicationId = "com.collabtable.app"
|
||||||
minSdk 26
|
minSdk = 26
|
||||||
targetSdk 34
|
targetSdk = 34
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary true
|
useSupportLibrary = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +38,10 @@ android {
|
|||||||
jvmTarget = '21'
|
jvmTarget = '21'
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose true
|
compose = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion '1.5.4'
|
kotlinCompilerExtensionVersion = '1.6.8'
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
@@ -56,7 +57,7 @@ dependencies {
|
|||||||
implementation 'androidx.activity:activity-compose:1.8.1'
|
implementation 'androidx.activity:activity-compose:1.8.1'
|
||||||
|
|
||||||
// Compose
|
// Compose
|
||||||
implementation platform('androidx.compose:compose-bom:2023.10.01')
|
implementation platform('androidx.compose:compose-bom:2024.06.00')
|
||||||
implementation 'androidx.compose.ui:ui'
|
implementation 'androidx.compose.ui:ui'
|
||||||
implementation 'androidx.compose.ui:ui-graphics'
|
implementation 'androidx.compose.ui:ui-graphics'
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
|
|||||||
@@ -2,11 +2,26 @@ package com.collabtable.app
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.collabtable.app.data.api.ApiClient
|
import com.collabtable.app.data.api.ApiClient
|
||||||
|
import com.collabtable.app.data.repository.SyncRepository
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class CollabTableApplication : Application() {
|
class CollabTableApplication : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
// Initialize API client with saved server URL
|
// Initialize API client with saved server URL
|
||||||
ApiClient.initialize(this)
|
ApiClient.initialize(this)
|
||||||
|
// Kick off a one-time full down-sync on cold app start if local DB is empty but server has data.
|
||||||
|
// This complements in-screen sync logic and ensures earliest possible hydration.
|
||||||
|
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
// Force a full down-sync on app start when server credentials are present.
|
||||||
|
SyncRepository(this@CollabTableApplication).performSync(forceFullDown = true)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Silent; UI layers will surface subsequent sync issues.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-3
@@ -1,6 +1,8 @@
|
|||||||
package com.collabtable.app.data.repository
|
package com.collabtable.app.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import com.collabtable.app.data.api.ApiClient
|
import com.collabtable.app.data.api.ApiClient
|
||||||
import com.collabtable.app.data.api.SyncRequest
|
import com.collabtable.app.data.api.SyncRequest
|
||||||
@@ -63,11 +65,17 @@ class SyncRepository(context: Context) {
|
|||||||
prefs.edit().putLong("last_local_sync_ts", ts).apply()
|
prefs.edit().putLong("last_local_sync_ts", ts).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun performSync(): Result<Unit> =
|
suspend fun performSync(forceFullDown: Boolean = false): Result<Unit> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// Prevent overlapping syncs across screens/viewmodels
|
// Prevent overlapping syncs across screens/viewmodels
|
||||||
syncMutex.withLock {
|
syncMutex.withLock {
|
||||||
try {
|
try {
|
||||||
|
// Only attempt network sync when app is in foreground to avoid background DNS issues
|
||||||
|
// Foreground check removed (lifecycle owner not available in this module); rely solely on network availability.
|
||||||
|
// Skip sync when there's no validated network available
|
||||||
|
if (!isNetworkAvailable()) {
|
||||||
|
return@withContext Result.success(Unit)
|
||||||
|
}
|
||||||
// If not authenticated, skip making network calls (e.g., after leaving server)
|
// If not authenticated, skip making network calls (e.g., after leaving server)
|
||||||
val prefMgr = PreferencesManager.getInstance(appContext)
|
val prefMgr = PreferencesManager.getInstance(appContext)
|
||||||
val currentPassword = prefMgr.getServerPassword()?.trim()
|
val currentPassword = prefMgr.getServerPassword()?.trim()
|
||||||
@@ -100,9 +108,16 @@ class SyncRepository(context: Context) {
|
|||||||
// lastSyncTimestamp to 0 for this request. This avoids permanently missing historical rows.
|
// lastSyncTimestamp to 0 for this request. This avoids permanently missing historical rows.
|
||||||
val hasAnyLists = database.listDao().getAllListIds().isNotEmpty()
|
val hasAnyLists = database.listDao().getAllListIds().isNotEmpty()
|
||||||
val hasAnyItems = database.itemDao().getAllItemIds().isNotEmpty()
|
val hasAnyItems = database.itemDao().getAllItemIds().isNotEmpty()
|
||||||
val effectiveLastServerTs = if (lastServerTs > 0 && (!hasAnyLists || !hasAnyItems)) 0L else lastServerTs
|
val effectiveLastServerTs = when {
|
||||||
|
// App start can request a full down-sync regardless of local contents
|
||||||
|
forceFullDown && lastServerTs > 0L -> 0L
|
||||||
|
// Fallback: if local looks empty but we have a watermark, do a one-time full down-sync
|
||||||
|
lastServerTs > 0L && (!hasAnyLists || !hasAnyItems) -> 0L
|
||||||
|
else -> lastServerTs
|
||||||
|
}
|
||||||
if (effectiveLastServerTs == 0L && lastServerTs > 0L) {
|
if (effectiveLastServerTs == 0L && lastServerTs > 0L) {
|
||||||
Logger.w("Sync", "Local store empty but watermark set; performing one-time full down-sync")
|
val reason = if (forceFullDown) "startup forced" else "local empty"
|
||||||
|
Logger.w("Sync", "$reason full down-sync requested")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather local changes since last sync
|
// Gather local changes since last sync
|
||||||
@@ -273,4 +288,16 @@ class SyncRepository(context: Context) {
|
|||||||
companion object {
|
companion object {
|
||||||
private val syncMutex = Mutex()
|
private val syncMutex = Mutex()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isNetworkAvailable(): Boolean {
|
||||||
|
val cm = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val network = cm.activeNetwork ?: return false
|
||||||
|
val caps = cm.getNetworkCapabilities(network) ?: return false
|
||||||
|
val hasInternet = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
val isValidated = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
val hasTransport = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||||
|
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
|
||||||
|
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
return hasInternet && isValidated && hasTransport
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-41
@@ -66,11 +66,13 @@ 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.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
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.zIndex
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
@@ -124,15 +126,15 @@ fun ListDetailScreen(
|
|||||||
// Use derivedStateOf to create stable references
|
// Use derivedStateOf to create stable references
|
||||||
val stableFields by remember { derivedStateOf { fields } }
|
val stableFields by remember { derivedStateOf { fields } }
|
||||||
val stableItems by remember { derivedStateOf { items } }
|
val stableItems by remember { derivedStateOf { items } }
|
||||||
|
|
||||||
var showManageColumnsDialog by remember { mutableStateOf(false) }
|
|
||||||
var showAddItemDialog by remember { mutableStateOf(false) }
|
|
||||||
var pendingScrollToBottom by remember { mutableStateOf(false) }
|
|
||||||
var itemToEdit by remember { mutableStateOf<ItemWithValues?>(null) }
|
var itemToEdit by remember { mutableStateOf<ItemWithValues?>(null) }
|
||||||
var showSortDialog by remember { mutableStateOf(false) }
|
var showSortDialog by remember { mutableStateOf(false) }
|
||||||
var showGroupDialog by remember { mutableStateOf(false) }
|
var showGroupDialog by remember { mutableStateOf(false) }
|
||||||
var showFilterDialog by remember { mutableStateOf(false) }
|
var showFilterDialog by remember { mutableStateOf(false) }
|
||||||
var showRenameListDialog by remember { mutableStateOf(false) }
|
var showRenameListDialog by remember { mutableStateOf(false) }
|
||||||
|
var showManageColumnsDialog by remember { mutableStateOf(false) }
|
||||||
|
var showAddItemDialog by remember { mutableStateOf(false) }
|
||||||
|
// Flag to trigger scroll to bottom after adding an item
|
||||||
|
var pendingScrollToBottom by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Filter/Sort state
|
// Filter/Sort state
|
||||||
var sortField by remember { mutableStateOf<Field?>(null) }
|
var sortField by remember { mutableStateOf<Field?>(null) }
|
||||||
@@ -162,7 +164,7 @@ fun ListDetailScreen(
|
|||||||
// Scaffold with top bar and FAB
|
// Scaffold with top bar and FAB
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
SmallTopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.clickable { showRenameListDialog = true },
|
modifier = Modifier.clickable { showRenameListDialog = true },
|
||||||
@@ -183,9 +185,9 @@ fun ListDetailScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
val context = LocalContext.current
|
val localCtx = LocalContext.current
|
||||||
val prefs = remember { PreferencesManager.getInstance(context) }
|
val localPrefs = remember { PreferencesManager.getInstance(localCtx) }
|
||||||
ConnectionStatusAction(prefs = prefs)
|
ConnectionStatusAction(prefs = localPrefs)
|
||||||
IconButton(onClick = { showManageColumnsDialog = true }) {
|
IconButton(onClick = { showManageColumnsDialog = true }) {
|
||||||
Icon(Icons.Default.Settings, contentDescription = "Manage Columns")
|
Icon(Icons.Default.Settings, contentDescription = "Manage Columns")
|
||||||
}
|
}
|
||||||
@@ -450,36 +452,7 @@ fun ListDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field headers with long-press to delete and resize handles
|
// Header will be rendered as a stickyHeader inside the LazyColumn below
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.horizontalScroll(horizontalScrollState),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
stableFields.forEach { field ->
|
|
||||||
key(field.id) {
|
|
||||||
FieldHeader(
|
|
||||||
field = field,
|
|
||||||
width = fieldWidths[field.id] ?: 150.dp,
|
|
||||||
onWidthChange = { delta ->
|
|
||||||
val currentWidth = fieldWidths[field.id] ?: 150.dp
|
|
||||||
// Enforce only a minimum width; remove the previous max (400dp)
|
|
||||||
val newWidth = (currentWidth.value + delta).coerceAtLeast(100f)
|
|
||||||
fieldWidths[field.id] = newWidth.dp
|
|
||||||
// Persist updated widths for this listId
|
|
||||||
prefs.setColumnWidths(
|
|
||||||
listId,
|
|
||||||
fieldWidths.mapValues { it.value.value },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
scrollState = horizontalScrollState,
|
|
||||||
isLast = (field.id == stableFields.lastOrNull()?.id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items list with synchronized scrolling
|
// Items list with synchronized scrolling
|
||||||
if (stableItems.isEmpty()) {
|
if (stableItems.isEmpty()) {
|
||||||
@@ -528,9 +501,19 @@ fun ListDetailScreen(
|
|||||||
pendingScrollToBottom = false
|
pendingScrollToBottom = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Measure header height so we can allow content to scroll "behind" it
|
||||||
|
// but still make the last item fully visible by providing extra bottom padding.
|
||||||
|
var headerHeightPx by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = listState,
|
state = listState,
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 0.dp,
|
||||||
|
bottom = with(density) { headerHeightPx.toDp() },
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
groupedItems.entries.forEach { (groupName, groupItems) ->
|
groupedItems.entries.forEach { (groupName, groupItems) ->
|
||||||
// Show group header if grouping is enabled
|
// Show group header if grouping is enabled
|
||||||
@@ -587,6 +570,41 @@ fun ListDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overlaid header: content scrolls behind this until the end,
|
||||||
|
// thanks to the extra bottom padding applied above.
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned { coordinates -> headerHeightPx = coordinates.size.height }
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.horizontalScroll(horizontalScrollState)
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.zIndex(1f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
stableFields.forEach { field ->
|
||||||
|
key(field.id) {
|
||||||
|
FieldHeader(
|
||||||
|
field = field,
|
||||||
|
width = fieldWidths[field.id] ?: 150.dp,
|
||||||
|
onWidthChange = { delta ->
|
||||||
|
val currentWidth = fieldWidths[field.id] ?: 150.dp
|
||||||
|
val newWidth = (currentWidth.value + delta).coerceAtLeast(100f)
|
||||||
|
fieldWidths[field.id] = newWidth.dp
|
||||||
|
prefs.setColumnWidths(
|
||||||
|
listId,
|
||||||
|
fieldWidths.mapValues { it.value.value },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scrollState = horizontalScrollState,
|
||||||
|
isLast = (field.id == stableFields.lastOrNull()?.id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1678,7 +1696,7 @@ fun EditFieldDialog(
|
|||||||
onUpdate: (String, String, String) -> Unit,
|
onUpdate: (String, String, String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf(field.name) }
|
var name by remember { mutableStateOf(field.name) }
|
||||||
var selectedFieldType by remember { mutableStateOf(field.fieldType?.uppercase() ?: "TEXT") }
|
var selectedFieldType by remember { mutableStateOf(field.fieldType.uppercase()) }
|
||||||
var dropdownOptions by remember { mutableStateOf(field.getDropdownOptions().joinToString(", ")) }
|
var dropdownOptions by remember { mutableStateOf(field.getDropdownOptions().joinToString(", ")) }
|
||||||
var currency by remember { mutableStateOf(field.getCurrency()) }
|
var currency by remember { mutableStateOf(field.getCurrency()) }
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
@@ -2478,8 +2496,6 @@ fun FieldInput(
|
|||||||
val options = field.getAutocompleteOptions()
|
val options = field.getAutocompleteOptions()
|
||||||
if (options.isNotEmpty()) {
|
if (options.isNotEmpty()) {
|
||||||
Text("Suggestions: ${options.take(3).joinToString(", ")}")
|
Text("Suggestions: ${options.take(3).joinToString(", ")}")
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -2853,6 +2869,7 @@ fun ManageColumnsDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
@Composable
|
@Composable
|
||||||
fun ColumnItem(
|
fun ColumnItem(
|
||||||
field: Field,
|
field: Field,
|
||||||
|
|||||||
+2
@@ -11,6 +11,7 @@ import com.collabtable.app.data.model.Item
|
|||||||
import com.collabtable.app.data.model.ItemValue
|
import com.collabtable.app.data.model.ItemValue
|
||||||
import com.collabtable.app.data.model.ItemWithValues
|
import com.collabtable.app.data.model.ItemWithValues
|
||||||
import com.collabtable.app.data.repository.SyncRepository
|
import com.collabtable.app.data.repository.SyncRepository
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -18,6 +19,7 @@ import kotlinx.coroutines.flow.debounce
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
class ListDetailViewModel(
|
class ListDetailViewModel(
|
||||||
private val database: CollabTableDatabase,
|
private val database: CollabTableDatabase,
|
||||||
private val listId: String,
|
private val listId: String,
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
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
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -122,7 +122,7 @@ fun LogsScreen(onNavigateBack: () -> Unit) {
|
|||||||
title = { Text("Logs (${filteredLogs.size}/${logs.size})") },
|
title = { Text("Logs (${filteredLogs.size}/${logs.size})") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.Filled.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -340,7 +340,7 @@ fun FilterBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
// Time range filter
|
// Time range filter
|
||||||
Text(
|
Text(
|
||||||
@@ -371,7 +371,7 @@ fun FilterBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
// Tag filters
|
// Tag filters
|
||||||
Text(
|
Text(
|
||||||
@@ -443,7 +443,7 @@ fun FilterBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
|
||||||
|
|
||||||
// Reset button
|
// Reset button
|
||||||
Button(
|
Button(
|
||||||
|
|||||||
+13
-17
@@ -6,16 +6,16 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.DarkMode
|
import androidx.compose.material.icons.automirrored.filled.List
|
||||||
import androidx.compose.material.icons.filled.LightMode
|
import androidx.compose.material.icons.filled.Brightness4
|
||||||
import androidx.compose.material.icons.filled.List
|
import androidx.compose.material.icons.filled.Brightness7
|
||||||
import androidx.compose.material.icons.filled.SettingsBrightness
|
import androidx.compose.material.icons.filled.SettingsBrightness
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -30,9 +30,9 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -42,21 +42,17 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
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 com.collabtable.app.ui.components.ConnectionStatusAction
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import com.collabtable.app.R
|
import com.collabtable.app.R
|
||||||
import com.collabtable.app.data.api.ApiClient
|
import com.collabtable.app.data.api.ApiClient
|
||||||
import com.collabtable.app.data.database.CollabTableDatabase
|
import com.collabtable.app.data.database.CollabTableDatabase
|
||||||
import com.collabtable.app.data.preferences.PreferencesManager
|
import com.collabtable.app.data.preferences.PreferencesManager
|
||||||
import com.collabtable.app.data.repository.SyncRepository
|
import com.collabtable.app.data.repository.SyncRepository
|
||||||
|
import com.collabtable.app.ui.components.ConnectionStatusAction
|
||||||
import com.collabtable.app.utils.Logger
|
import com.collabtable.app.utils.Logger
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -95,7 +91,7 @@ fun SettingsScreen(
|
|||||||
title = { Text(stringResource(R.string.settings)) },
|
title = { Text(stringResource(R.string.settings)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@@ -165,19 +161,19 @@ fun SettingsScreen(
|
|||||||
selected = themeMode == PreferencesManager.THEME_MODE_SYSTEM,
|
selected = themeMode == PreferencesManager.THEME_MODE_SYSTEM,
|
||||||
onClick = { preferencesManager.setThemeMode(PreferencesManager.THEME_MODE_SYSTEM) },
|
onClick = { preferencesManager.setThemeMode(PreferencesManager.THEME_MODE_SYSTEM) },
|
||||||
label = { Text("System") },
|
label = { Text("System") },
|
||||||
leadingIcon = { Icon(Icons.Default.SettingsBrightness, contentDescription = null) },
|
leadingIcon = { Icon(Icons.Filled.SettingsBrightness, contentDescription = null) },
|
||||||
)
|
)
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = themeMode == PreferencesManager.THEME_MODE_LIGHT,
|
selected = themeMode == PreferencesManager.THEME_MODE_LIGHT,
|
||||||
onClick = { preferencesManager.setThemeMode(PreferencesManager.THEME_MODE_LIGHT) },
|
onClick = { preferencesManager.setThemeMode(PreferencesManager.THEME_MODE_LIGHT) },
|
||||||
label = { Text("Light") },
|
label = { Text("Light") },
|
||||||
leadingIcon = { Icon(Icons.Default.LightMode, contentDescription = null) },
|
leadingIcon = { Icon(Icons.Filled.Brightness7, contentDescription = null) },
|
||||||
)
|
)
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = themeMode == PreferencesManager.THEME_MODE_DARK,
|
selected = themeMode == PreferencesManager.THEME_MODE_DARK,
|
||||||
onClick = { preferencesManager.setThemeMode(PreferencesManager.THEME_MODE_DARK) },
|
onClick = { preferencesManager.setThemeMode(PreferencesManager.THEME_MODE_DARK) },
|
||||||
label = { Text("Dark") },
|
label = { Text("Dark") },
|
||||||
leadingIcon = { Icon(Icons.Default.DarkMode, contentDescription = null) },
|
leadingIcon = { Icon(Icons.Filled.Brightness4, contentDescription = null) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +296,7 @@ fun SettingsScreen(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
Button(onClick = onNavigateToLogs, modifier = Modifier.fillMaxWidth()) {
|
Button(onClick = onNavigateToLogs, modifier = Modifier.fillMaxWidth()) {
|
||||||
Icon(Icons.Default.List, contentDescription = null)
|
Icon(Icons.AutoMirrored.Filled.List, contentDescription = null)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text("Open Logs")
|
Text("Open Logs")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.3.0' apply false
|
id 'com.android.application' version '8.8.0' apply false
|
||||||
id 'com.android.library' version '8.3.0' apply false
|
id 'com.android.library' version '8.8.0' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
|
id 'org.jetbrains.kotlin.android' version '2.0.21' apply false
|
||||||
id 'com.google.devtools.ksp' version '1.9.20-1.0.14' apply false
|
id 'org.jetbrains.kotlin.plugin.compose' version '2.0.21' apply false
|
||||||
|
id 'com.google.devtools.ksp' version '2.0.21-1.0.27' apply false
|
||||||
id 'org.jlleitschuh.gradle.ktlint' version '12.1.0' apply false
|
id 'org.jlleitschuh.gradle.ktlint' version '12.1.0' apply false
|
||||||
id 'io.gitlab.arturbosch.detekt' version '1.23.6' apply false
|
id 'io.gitlab.arturbosch.detekt' version '1.23.6' apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
Reference in New Issue
Block a user