feat: update dependencies and enhance sync logic for improved performance and reliability

This commit is contained in:
2025-11-08 21:24:38 +01:00
parent 0bc3b3b06f
commit aa2542a0ac
9 changed files with 164 additions and 105 deletions
+10 -9
View File
@@ -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.
}
}
} }
} }
@@ -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
}
} }
@@ -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,
@@ -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(
@@ -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")
} }
+5 -4
View File
@@ -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