Implementation SQLDelight database
This commit is contained in:
+11
-1
@@ -1,10 +1,20 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.di
|
||||
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||
import com.digitoolsolutions.app.torquevaultkmp.AppStoragePlatform
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
actual val platformModule = module {
|
||||
single<AppStorage> { AppStoragePlatform(androidContext()) }
|
||||
}
|
||||
single<SqlDriver> {
|
||||
AndroidSqliteDriver(
|
||||
schema = AppDatabase.Schema,
|
||||
context = androidContext(),
|
||||
name = "app.db"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.data.repository
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabaseQueries
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Device
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class DeviceRepository(
|
||||
private val dbQueries: AppDatabaseQueries
|
||||
) {
|
||||
suspend fun saveDevice(id: String, name: String) {
|
||||
dbQueries.insertDevice(id, name)
|
||||
}
|
||||
|
||||
fun getAllDevices(): Flow<List<Device>> =
|
||||
dbQueries.getAllDevices()
|
||||
.asFlow()
|
||||
.mapToList(Dispatchers.IO)
|
||||
|
||||
suspend fun getDevicesPaged(limit: Long, lastId: String?): List<Device> {
|
||||
return if (lastId == null) {
|
||||
dbQueries.getDevicesFirstPage(limit).executeAsList()
|
||||
} else {
|
||||
dbQueries.getDevicesNextPage(lastId = lastId, limit = limit).executeAsList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getDeviceById(id: String): Device? =
|
||||
dbQueries.getDeviceById(id).executeAsOneOrNull()
|
||||
|
||||
suspend fun deleteDevice(id: String) = dbQueries.deleteDeviceById(id)
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.data.repository
|
||||
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabaseQueries
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Log
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Clock
|
||||
|
||||
class LogRepository(
|
||||
private val dbQueries: AppDatabaseQueries
|
||||
) {
|
||||
suspend fun addLog(title: String, content: String, deviceId: String? = null) {
|
||||
val source = if (deviceId == null) LogFilter.System.name else LogFilter.Device.name
|
||||
dbQueries.insertLog(
|
||||
title = title,
|
||||
content = content,
|
||||
timestamp = Clock.System.now().toEpochMilliseconds(),
|
||||
deviceId = deviceId,
|
||||
source = source
|
||||
)
|
||||
}
|
||||
|
||||
fun getAllLogs(): Flow<List<Log>> =
|
||||
dbQueries.getAllLogs()
|
||||
.asFlow()
|
||||
.mapToList(Dispatchers.IO)
|
||||
|
||||
suspend fun getLogsPaged(limit: Long, lastTimestamp: Long?, lastId: Long?): List<Log> {
|
||||
return if (lastTimestamp == null || lastId == null) {
|
||||
dbQueries.getLogsFirstPage(limit).executeAsList()
|
||||
} else {
|
||||
dbQueries.getLogsNextPage(lastTimestamp = lastTimestamp, lastId = lastId, limit = limit).executeAsList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLogsByFilterPaged(filter: LogFilter, limit: Long, lastTimestamp: Long?, lastId: Long?): List<Log> {
|
||||
return when (filter) {
|
||||
LogFilter.All -> getLogsPaged(limit, lastTimestamp, lastId)
|
||||
LogFilter.System, LogFilter.Device -> {
|
||||
if (lastTimestamp == null || lastId == null) {
|
||||
dbQueries.getLogsByTypeFirstPage(filter.name, limit).executeAsList()
|
||||
} else {
|
||||
dbQueries.getLogsByTypeNextPage(source = filter.name, lastTimestamp = lastTimestamp, lastId = lastId, limit = limit).executeAsList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLogsByDevicePaged(deviceId: String, limit: Long, lastTimestamp: Long?, lastId: Long?): List<Log> {
|
||||
return if (lastTimestamp == null || lastId == null) {
|
||||
dbQueries.getLogsByDeviceFirstPage(deviceId, limit).executeAsList()
|
||||
} else {
|
||||
dbQueries.getLogsByDeviceNextPage(deviceId = deviceId, lastTimestamp = lastTimestamp, lastId = lastId, limit = limit).executeAsList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearLogs() = dbQueries.clearAllLogs()
|
||||
}
|
||||
@@ -8,6 +8,7 @@ fun initKoin(config: KoinAppDeclaration? = null) {
|
||||
config?.invoke(this)
|
||||
modules(
|
||||
appModule,
|
||||
storageModule,
|
||||
networkModule,
|
||||
viewModelModule,
|
||||
platformModule
|
||||
|
||||
+14
@@ -4,12 +4,17 @@ import com.digitoolsolutions.app.torquevaultkmp.AppViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.createPlatformHttpClient
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.AuthApi
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.WorkOrderApi
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.DeviceRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.LogRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderRepositoryImpl
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.usecase.FetchWorkOrdersUseCase
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.auth.AuthViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.BondedViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingViewModel
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.module.dsl.factoryOf
|
||||
@@ -24,6 +29,13 @@ val appModule = module {
|
||||
single { TokenManager(get()) }
|
||||
}
|
||||
|
||||
val storageModule = module {
|
||||
single { AppDatabase(get()) }
|
||||
single { get<AppDatabase>().appDatabaseQueries }
|
||||
single { LogRepository(get()) }
|
||||
single { DeviceRepository(get()) }
|
||||
}
|
||||
|
||||
val networkModule = module {
|
||||
// Auth client (no interceptor to avoid circular dependency and recursion)
|
||||
single(named("authClient")) { createPlatformHttpClient(get(), null) }
|
||||
@@ -44,4 +56,6 @@ val viewModelModule = module {
|
||||
factory { AuthViewModel(get(), get()) }
|
||||
singleOf(::AppViewModel)
|
||||
factoryOf(::SettingViewModel)
|
||||
factoryOf(::BondedViewModel)
|
||||
factoryOf(::LogViewModel)
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.domain.model
|
||||
|
||||
enum class LogFilter {
|
||||
All,
|
||||
System,
|
||||
Device;
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): LogFilter {
|
||||
return entries.find { it.name == value } ?: All
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
-53
@@ -1,9 +1,19 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bonded
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@@ -11,47 +21,76 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Device
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.view.BondedDeviceItem
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.view.EmptyBondedView
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import torquevaultkmp.composeapp.generated.resources.Res
|
||||
import torquevaultkmp.composeapp.generated.resources.bonded_title
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BondedScreen(
|
||||
navController: NavController,
|
||||
// bleScannerViewModel: ScannerViewModel,
|
||||
// viewModel: BondedViewModel = hiltViewModel()
|
||||
viewModel: BondedViewModel = koinViewModel()
|
||||
) {
|
||||
// val devices = viewModel.bondedDevices.collectAsLazyPagingItems()
|
||||
val devices by viewModel.bondedDevices.collectAsState()
|
||||
val isLoading by viewModel.isLoading.collectAsState()
|
||||
|
||||
BondedContent(
|
||||
// devices = devices,
|
||||
// onRefresh = {devices.refresh()},
|
||||
// onDelete = {
|
||||
// viewModel.removeDevice(it)
|
||||
// bleScannerViewModel.removeBondedCache(it.id)
|
||||
// }
|
||||
devices = devices,
|
||||
isLoading = isLoading,
|
||||
onAddFake = { viewModel.addFakeDevice() },
|
||||
onDelete = { viewModel.removeDevice(it.id) },
|
||||
onLoadMore = { viewModel.loadNextPage() },
|
||||
onRefresh = { viewModel.refresh() }
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun BondedContent(
|
||||
// devices: LazyPagingItems<Device>,
|
||||
// onRefresh: () -> Unit,
|
||||
// onDelete: (Device) -> Unit
|
||||
devices: List<Device>,
|
||||
isLoading: Boolean,
|
||||
onAddFake: () -> Unit,
|
||||
onDelete: (Device) -> Unit,
|
||||
onLoadMore: () -> Unit,
|
||||
onRefresh: () -> Unit
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// Infinite Scroll logic
|
||||
val shouldLoadMore = remember {
|
||||
derivedStateOf {
|
||||
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||
lastVisibleItemIndex >= devices.size - 5 && devices.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldLoadMore) {
|
||||
snapshotFlow { shouldLoadMore.value }
|
||||
.distinctUntilChanged()
|
||||
.filter { it }
|
||||
.collect {
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold (
|
||||
modifier = Modifier.imePadding(),
|
||||
topBar = {
|
||||
@@ -59,50 +98,41 @@ internal fun BondedContent(
|
||||
title = {Text(stringResource(Res.string.bonded_title))},
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = onAddFake) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add Fake Device")
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) })
|
||||
{ padding ->
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
val scope = rememberCoroutineScope()
|
||||
PullToRefreshBox(
|
||||
isRefreshing = false,
|
||||
isRefreshing = isLoading && devices.isEmpty(),
|
||||
onRefresh = {
|
||||
// onRefresh()
|
||||
scope.launch {
|
||||
pullToRefreshState.animateToHidden()
|
||||
}
|
||||
onRefresh()
|
||||
},
|
||||
state = pullToRefreshState,
|
||||
modifier = Modifier.fillMaxSize().imePadding().padding(padding).padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
){
|
||||
// if (devices.itemCount == 0) {
|
||||
// EmptyBondedView()
|
||||
// }else {
|
||||
// LazyColumn{
|
||||
// items(count = devices.itemCount, key = { index -> devices[index]?.id ?: index }) { index ->
|
||||
// val device = devices[index]
|
||||
// device?.let { it ->
|
||||
// BondedDeviceItem(it, onDelete = { onDelete(it) })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if (devices.isEmpty() && !isLoading) {
|
||||
EmptyBondedView()
|
||||
} else {
|
||||
LazyColumn(state = listState) {
|
||||
items(items = devices, key = { it.id }) { device ->
|
||||
BondedDeviceItem(device, onDelete = { onDelete(it) })
|
||||
}
|
||||
if (isLoading && devices.isNotEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun LogScreenPreview() {
|
||||
// val fakeDevices = listOf(
|
||||
// Device("AA:BB:CC:DD:EE", "Device 1"),
|
||||
// Device("AA:BB:CC:DD:FF", "Device 2")
|
||||
// )
|
||||
// val pagingData = PagingData.from(fakeDevices)
|
||||
// val lazyPagingItems = flowOf(pagingData).collectAsLazyPagingItems()
|
||||
//
|
||||
// BondedContent(
|
||||
// devices = lazyPagingItems,
|
||||
// onRefresh = {lazyPagingItems.refresh()},
|
||||
// onDelete = {}
|
||||
// )
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bonded
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.DeviceRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Device
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.random.Random
|
||||
|
||||
class BondedViewModel(
|
||||
private val deviceRepository: DeviceRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _bondedDevices = MutableStateFlow<List<Device>>(emptyList())
|
||||
val bondedDevices: StateFlow<List<Device>> = _bondedDevices.asStateFlow()
|
||||
|
||||
private var lastId: String? = null
|
||||
private val pageSize = 20L
|
||||
private var isLastPage = false
|
||||
var isLoading = MutableStateFlow(false)
|
||||
private set
|
||||
|
||||
init {
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
if (isLoading.value || isLastPage) return
|
||||
|
||||
viewModelScope.launch {
|
||||
isLoading.value = true
|
||||
val newDevices = deviceRepository.getDevicesPaged(limit = pageSize, lastId = lastId)
|
||||
|
||||
if (newDevices.size < pageSize) {
|
||||
isLastPage = true
|
||||
}
|
||||
|
||||
if (newDevices.isNotEmpty()) {
|
||||
lastId = newDevices.last().id
|
||||
_bondedDevices.value += newDevices
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
lastId = null
|
||||
isLastPage = false
|
||||
_bondedDevices.value = emptyList()
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
fun addFakeDevice() {
|
||||
viewModelScope.launch {
|
||||
val id = "AA:BB:CC:DD:EE:${Random.nextInt(10, 99)}"
|
||||
val name = "Torque Tool ${Random.nextInt(1, 100)}"
|
||||
deviceRepository.saveDevice(id, name)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeDevice(id: String) {
|
||||
viewModelScope.launch {
|
||||
deviceRepository.deleteDevice(id)
|
||||
// If we remove an item, we might just update the local list
|
||||
_bondedDevices.value = _bondedDevices.value.filter { it.id != id }
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-23
@@ -24,6 +24,7 @@ import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -32,13 +33,22 @@ import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Device
|
||||
|
||||
@Composable
|
||||
fun BondedDeviceItem(/** device: Device, onDelete: (Device) -> Unit,*/ modifier: Modifier = Modifier) {
|
||||
fun BondedDeviceItem(device: Device, onDelete: (Device) -> Unit, modifier: Modifier = Modifier) {
|
||||
val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
|
||||
SwipeToDismissBoxValue.Settled,
|
||||
SwipeToDismissBoxDefaults.positionalThreshold
|
||||
)
|
||||
|
||||
if (swipeToDismissBoxState.currentValue == SwipeToDismissBoxValue.EndToStart) {
|
||||
LaunchedEffect(Unit) {
|
||||
onDelete(device)
|
||||
swipeToDismissBoxState.snapTo(SwipeToDismissBoxValue.Settled)
|
||||
}
|
||||
}
|
||||
|
||||
SwipeToDismissBox(
|
||||
state = swipeToDismissBoxState,
|
||||
modifier = modifier.fillMaxSize().padding(vertical = 4.dp),
|
||||
@@ -46,13 +56,14 @@ fun BondedDeviceItem(/** device: Device, onDelete: (Device) -> Unit,*/ modifier:
|
||||
backgroundContent = {
|
||||
when (swipeToDismissBoxState.dismissDirection) {
|
||||
SwipeToDismissBoxValue.StartToEnd -> {
|
||||
SwipeToDismissBackgroundItem(label = "Swipe to delete item", contentAlignment = Alignment.CenterStart, state = swipeToDismissBoxState)
|
||||
SwipeToDismissBackgroundItem(label = "Delete", contentAlignment = Alignment.CenterStart, state = swipeToDismissBoxState)
|
||||
}
|
||||
SwipeToDismissBoxValue.EndToStart -> {
|
||||
SwipeToDismissBackgroundItem(label = "Delete", state = swipeToDismissBoxState)
|
||||
}
|
||||
SwipeToDismissBoxValue.EndToStart -> {SwipeToDismissBackgroundItem(label = "Swipe to delete item", state = swipeToDismissBoxState)}
|
||||
SwipeToDismissBoxValue.Settled -> {}
|
||||
}
|
||||
},
|
||||
// onDismiss = {onDelete(device)},
|
||||
}
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -65,15 +76,15 @@ fun BondedDeviceItem(/** device: Device, onDelete: (Device) -> Unit,*/ modifier:
|
||||
CircularIcon( imageVector = Icons.Default.Bluetooth, backgroundColor = Color(0xFF0082FC))
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column {
|
||||
// Text(
|
||||
// text = device.name,
|
||||
// style = MaterialTheme.typography.titleMedium
|
||||
// )
|
||||
// Text(
|
||||
// text = device.id,
|
||||
// style = MaterialTheme.typography.bodyMedium,
|
||||
// color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
// )
|
||||
Text(
|
||||
text = device.name,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = device.id,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,12 +122,3 @@ internal fun SwipeToDismissBackgroundItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun BondedDeviceItemPreview() {
|
||||
BondedDeviceItem(
|
||||
// Device("AA:BB:CC:DD:EE", "Device 1"),
|
||||
// onDelete = {}
|
||||
)
|
||||
}
|
||||
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.logs//package com.digitoolsolutions.app.torquevaultkmp.screens.logs
|
||||
//
|
||||
//import androidx.compose.runtime.Composable
|
||||
//import androidx.compose.runtime.Stable
|
||||
//import androidx.compose.runtime.getValue
|
||||
//import androidx.compose.runtime.mutableStateOf
|
||||
//import androidx.compose.runtime.saveable.Saver
|
||||
//import androidx.compose.runtime.saveable.rememberSaveable
|
||||
//import androidx.compose.runtime.setValue
|
||||
//import com.digitoolsolutions.android.storage.model.LogFilter
|
||||
//
|
||||
//@Stable
|
||||
//interface LogFilterState {
|
||||
// val filters: List<LogFilter>
|
||||
//// val sortingOptions: List<LogSortOption>
|
||||
// val activeFilter: LogFilter
|
||||
//// val activeSort: LogSortOption
|
||||
// val isEmpty: Boolean
|
||||
//
|
||||
// fun setFilter(filter: LogFilter)
|
||||
//// fun sortBy(option: LogSortOption)
|
||||
// fun clear()
|
||||
//}
|
||||
//
|
||||
//val logFilterSaver = Saver<LogFilter, String>(
|
||||
// save = { filter ->
|
||||
// when (filter) {
|
||||
// LogFilter.All -> "All"
|
||||
// LogFilter.System -> "System"
|
||||
// LogFilter.Device -> "Device"
|
||||
// is LogFilter.ByDeviceId -> "ByDeviceId:${filter.deviceId}"
|
||||
// }
|
||||
// },
|
||||
// restore = { str ->
|
||||
// when {
|
||||
// str == "All" -> LogFilter.All
|
||||
// str == "System" -> LogFilter.System
|
||||
// str == "Device" -> LogFilter.Device
|
||||
// str.startsWith("ByDeviceId:") -> LogFilter.ByDeviceId(str.removePrefix("ByDeviceId:"))
|
||||
// else -> LogFilter.All
|
||||
// }
|
||||
// }
|
||||
//)
|
||||
//
|
||||
////val logSortSaver = Saver<LogSortOption, String>(
|
||||
//// save = { sort ->
|
||||
//// when (sort) {
|
||||
//// LogSortOption.ByDate -> "ByDate"
|
||||
//// }
|
||||
//// },
|
||||
//// restore = { str ->
|
||||
//// when (str) {
|
||||
//// "ByDate" -> LogSortOption.ByDate
|
||||
//// else -> LogSortOption.ByDate
|
||||
//// }
|
||||
//// }
|
||||
////)
|
||||
//
|
||||
//
|
||||
//@Composable
|
||||
//fun rememberLogFilterState(
|
||||
// filters: List<LogFilter> = listOf(LogFilter.All, LogFilter.System, LogFilter.Device),
|
||||
//// sortingOptions: List<LogSortOption> = listOf()
|
||||
//): LogFilterState {
|
||||
// var selectedFilter by rememberSaveable(stateSaver = logFilterSaver) {
|
||||
// mutableStateOf<LogFilter>(LogFilter.All)
|
||||
// }
|
||||
//// var selectedSort by rememberSaveable(stateSaver = logSortSaver) {
|
||||
//// mutableStateOf<LogSortOption>(LogSortOption.ByDate)
|
||||
//// }
|
||||
//
|
||||
// return object : LogFilterState {
|
||||
// override val filters = filters
|
||||
//// override val sortingOptions = sortingOptions
|
||||
// override val activeFilter: LogFilter = selectedFilter
|
||||
//// override val activeSort: LogSortOption = selectedSort
|
||||
// override val isEmpty: Boolean get() = selectedFilter == LogFilter.All // && selectedSort == LogSortOption.ByDate
|
||||
//
|
||||
// override fun setFilter(filter: LogFilter) { selectedFilter = filter }
|
||||
//// override fun sortBy(option: LogSortOption) { selectedSort = option }
|
||||
// override fun clear() {
|
||||
// selectedFilter = LogFilter.All
|
||||
//// selectedSort = LogSortOption.ByDate
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
+93
-58
@@ -1,13 +1,21 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.logs
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.DeleteForever
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -18,53 +26,89 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.FilterBottomSheet
|
||||
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
||||
import kotlinx.coroutines.launch
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Log
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.EmptyLogView
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.FilterBottomSheet
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.LogItem
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import torquevaultkmp.composeapp.generated.resources.Res
|
||||
import torquevaultkmp.composeapp.generated.resources.log_title
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LogScreen(
|
||||
navController: NavController,
|
||||
// viewModel: LogViewModel = hiltViewModel()
|
||||
viewModel: LogViewModel = koinViewModel()
|
||||
) {
|
||||
// val state = rememberLogFilterState()
|
||||
// val logs = viewModel.getLogs(filter = state.activeFilter).collectAsLazyPagingItems()
|
||||
val logs by viewModel.logs.collectAsState()
|
||||
val activeFilter by viewModel.activeFilter.collectAsState()
|
||||
val isLoading by viewModel.isLoading.collectAsState()
|
||||
|
||||
LogContent(
|
||||
// state = state,
|
||||
// logs = logs,
|
||||
// onRefresh = {logs.refresh()},
|
||||
// onDeleteAllLogs = {viewModel.removeAllLog()},
|
||||
logs = logs,
|
||||
activeFilter = activeFilter,
|
||||
isLoading = isLoading,
|
||||
onFilterSelected = { viewModel.setFilter(it) },
|
||||
onAddFakeLog = { viewModel.addFakeLog() },
|
||||
onDeleteAllLogs = { viewModel.clearAllLogs() },
|
||||
onLoadMore = { viewModel.loadNextPage() },
|
||||
onRefresh = { viewModel.refresh() }
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun LogContent(
|
||||
// state: LogFilterState,
|
||||
// logs: LazyPagingItems<Log>,
|
||||
// onRefresh: () -> Unit,
|
||||
// onDeleteAllLogs: () -> Unit,
|
||||
logs: List<Log>,
|
||||
activeFilter: LogFilter,
|
||||
isLoading: Boolean,
|
||||
onFilterSelected: (LogFilter) -> Unit,
|
||||
onAddFakeLog: () -> Unit,
|
||||
onDeleteAllLogs: () -> Unit,
|
||||
onLoadMore: () -> Unit,
|
||||
onRefresh: () -> Unit
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// Infinite Scroll logic
|
||||
val shouldLoadMore = remember {
|
||||
derivedStateOf {
|
||||
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||
lastVisibleItemIndex >= logs.size - 5 && logs.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldLoadMore) {
|
||||
snapshotFlow { shouldLoadMore.value }
|
||||
.distinctUntilChanged()
|
||||
.filter { it }
|
||||
.collect {
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold (
|
||||
modifier = Modifier.imePadding(),
|
||||
topBar = {
|
||||
@@ -82,23 +126,25 @@ internal fun LogContent(
|
||||
Icon(
|
||||
Icons.Outlined.FilterList,
|
||||
contentDescription = "Filter logs",
|
||||
tint = Color.White
|
||||
tint = if (activeFilter == LogFilter.All) Color.White else Color.Yellow
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = onAddFakeLog) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add Fake Log")
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) })
|
||||
{ padding ->
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
PullToRefreshBox(
|
||||
isRefreshing = false,
|
||||
isRefreshing = isLoading && logs.isEmpty(),
|
||||
onRefresh = {
|
||||
// onRefresh()
|
||||
scope.launch {
|
||||
pullToRefreshState.animateToHidden()
|
||||
}
|
||||
onRefresh()
|
||||
},
|
||||
state = pullToRefreshState,
|
||||
modifier = Modifier
|
||||
@@ -107,18 +153,25 @@ internal fun LogContent(
|
||||
.padding(padding)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
){
|
||||
// if (logs.itemCount == 0) {
|
||||
// EmptyLogView()
|
||||
// }else {
|
||||
// LazyColumn{
|
||||
// items(logs.itemCount, key = { index -> logs[index]?.id ?: index }) { index ->
|
||||
// val log = logs[index]
|
||||
// log?.let { it ->
|
||||
// LogItem(it)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if (logs.isEmpty() && !isLoading) {
|
||||
EmptyLogView()
|
||||
} else {
|
||||
LazyColumn(state = listState) {
|
||||
items(items = logs, key = { it.id }) { log ->
|
||||
LogItem(log)
|
||||
}
|
||||
if (isLoading && logs.isNotEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showConfirmDialog) {
|
||||
AlertDialog(
|
||||
@@ -127,7 +180,7 @@ internal fun LogContent(
|
||||
text = { Text("Warning: This will permanently delete all current logs. This action cannot be undone.") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
// onDeleteAllLogs()
|
||||
onDeleteAllLogs()
|
||||
showConfirmDialog = false
|
||||
}) {
|
||||
Text("Delete", color = Color.Red)
|
||||
@@ -135,35 +188,17 @@ internal fun LogContent(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showConfirmDialog = false }) {
|
||||
Text("Cancel", color = Color.White)
|
||||
Text("Cancel", color = Color.Gray)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
if (showBottomSheet) {
|
||||
FilterBottomSheet(
|
||||
// state = state,
|
||||
activeFilter = activeFilter,
|
||||
onFilterSelected = onFilterSelected,
|
||||
onDismissRequest = {showBottomSheet = false}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun LogScreenPreview() {
|
||||
// val fakeDevices = listOf(
|
||||
// Log(1, "Log 1", "This is log 1"),
|
||||
// Log(2, "Log 2", "This is log 2"),
|
||||
// Log(3, "Log 3", "This is log 3"),
|
||||
// )
|
||||
// val pagingData = PagingData.from(fakeDevices)
|
||||
// val lazyPagingItems = flowOf(pagingData).collectAsLazyPagingItems()
|
||||
|
||||
LogContent(
|
||||
// state = rememberLogFilterState(),
|
||||
// logs = lazyPagingItems,
|
||||
// onRefresh = {lazyPagingItems.refresh()},
|
||||
// onDeleteAllLogs = {}
|
||||
)
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.logs
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.LogRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Log
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LogViewModel(
|
||||
private val logRepository: LogRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _logs = MutableStateFlow<List<Log>>(emptyList())
|
||||
val logs: StateFlow<List<Log>> = _logs.asStateFlow()
|
||||
|
||||
private val _activeFilter = MutableStateFlow(LogFilter.All)
|
||||
val activeFilter: StateFlow<LogFilter> = _activeFilter.asStateFlow()
|
||||
|
||||
private var lastTimestamp: Long? = null
|
||||
private var lastId: Long? = null
|
||||
private val pageSize = 20L
|
||||
private var isLastPage = false
|
||||
var isLoading = MutableStateFlow(false)
|
||||
private set
|
||||
|
||||
init {
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
fun setFilter(filter: LogFilter) {
|
||||
if (_activeFilter.value == filter) return
|
||||
_activeFilter.value = filter
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
if (isLoading.value || isLastPage) return
|
||||
|
||||
viewModelScope.launch {
|
||||
isLoading.value = true
|
||||
val newLogs = logRepository.getLogsByFilterPaged(
|
||||
filter = _activeFilter.value,
|
||||
limit = pageSize,
|
||||
lastTimestamp = lastTimestamp,
|
||||
lastId = lastId
|
||||
)
|
||||
|
||||
if (newLogs.size < pageSize) {
|
||||
isLastPage = true
|
||||
}
|
||||
|
||||
if (newLogs.isNotEmpty()) {
|
||||
val lastLog = newLogs.last()
|
||||
lastTimestamp = lastLog.timestamp
|
||||
lastId = lastLog.id
|
||||
_logs.value = _logs.value + newLogs
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
lastTimestamp = null
|
||||
lastId = null
|
||||
isLastPage = false
|
||||
_logs.value = emptyList()
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
fun addFakeLog() {
|
||||
viewModelScope.launch {
|
||||
val titles = listOf("System Alert", "Device Connected", "Torque Applied", "Calibration Sync")
|
||||
val contents = listOf("Voltage stable", "Tool #42 joined", "15.5 Nm recorded", "Sync successful")
|
||||
logRepository.addLog(
|
||||
title = titles.random(),
|
||||
content = contents.random()
|
||||
)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAllLogs() {
|
||||
viewModelScope.launch {
|
||||
logRepository.clearLogs()
|
||||
_logs.value = emptyList()
|
||||
lastTimestamp = null
|
||||
lastId = null
|
||||
isLastPage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-97
@@ -4,8 +4,8 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -26,8 +26,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import torquevaultkmp.composeapp.generated.resources.Res
|
||||
import torquevaultkmp.composeapp.generated.resources.clear
|
||||
@@ -35,13 +35,12 @@ import torquevaultkmp.composeapp.generated.resources.clear
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FilterBottomSheet(
|
||||
// state: LogFilterState,
|
||||
activeFilter: LogFilter,
|
||||
onFilterSelected: (LogFilter) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
onDismissRequest()
|
||||
},
|
||||
onDismissRequest = onDismissRequest,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 16.dp,
|
||||
@@ -56,88 +55,60 @@ fun FilterBottomSheet(
|
||||
)
|
||||
}
|
||||
) {
|
||||
FilterContent(/** state = state,*/ onDismissRequest = onDismissRequest)
|
||||
FilterContent(
|
||||
activeFilter = activeFilter,
|
||||
onFilterSelected = {
|
||||
onFilterSelected(it)
|
||||
onDismissRequest()
|
||||
},
|
||||
onClear = {
|
||||
onFilterSelected(LogFilter.All)
|
||||
onDismissRequest()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun FilterContent(
|
||||
// state: LogFilterState,
|
||||
activeFilter: LogFilter,
|
||||
onFilterSelected: (LogFilter) -> Unit,
|
||||
onClear: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
modifier = modifier.padding(bottom = 32.dp),
|
||||
) {
|
||||
Header(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = "Filter",
|
||||
isSelectedFilterEmpty = true ,// state.isEmpty,
|
||||
onFilterReset = {
|
||||
// state.clear()
|
||||
onDismissRequest()
|
||||
}
|
||||
isSelectedFilterEmpty = activeFilter == LogFilter.All,
|
||||
onFilterReset = onClear
|
||||
)
|
||||
|
||||
// if (state.filters.isNotEmpty()) {
|
||||
// FlowRow(
|
||||
// modifier = Modifier.padding(horizontal = 8.dp),
|
||||
// horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
// verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
// ) {
|
||||
// state.filters.forEach { filter ->
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
// .clip(RoundedCornerShape(8.dp))
|
||||
// .selectable(
|
||||
// selected = state.activeFilter == filter,
|
||||
// onClick = { state.setFilter(filter) },
|
||||
// role = Role.RadioButton
|
||||
// )
|
||||
// .padding(end = 16.dp),
|
||||
// verticalAlignment = Alignment.CenterVertically
|
||||
// ) {
|
||||
// RadioButton(
|
||||
// selected = state.activeFilter == filter,
|
||||
// onClick = { state.setFilter(filter) }
|
||||
// )
|
||||
// Text(filter.toString().replaceFirstChar { it.uppercase() })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (state.filters.isNotEmpty() && state.sortingOptions.isNotEmpty()) {
|
||||
// HorizontalDivider()
|
||||
//
|
||||
// Text(
|
||||
// text = "Sort by",
|
||||
// style = MaterialTheme.typography.titleMedium,
|
||||
// modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp)
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// if (state.sortingOptions.isNotEmpty()) {
|
||||
// state.sortingOptions.forEach { sort ->
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
// .clip(RoundedCornerShape(8.dp))
|
||||
// .selectable(
|
||||
// selected = state.activeSort == sort,
|
||||
// onClick = { state.sortBy(sort) },
|
||||
// role = Role.RadioButton
|
||||
// )
|
||||
// .padding(horizontal = 16.dp),
|
||||
// verticalAlignment = Alignment.CenterVertically
|
||||
// ) {
|
||||
// RadioButton(
|
||||
// selected = state.activeSort == sort,
|
||||
// onClick = { state.sortBy(sort) }
|
||||
// )
|
||||
// Text(sort.toString().replaceFirstChar { it.uppercase() })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
LogFilter.entries.forEach { filter ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = activeFilter == filter,
|
||||
onClick = { onFilterSelected(filter) },
|
||||
role = Role.RadioButton
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = activeFilter == filter,
|
||||
onClick = { onFilterSelected(filter) }
|
||||
)
|
||||
Text(
|
||||
text = filter.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +127,7 @@ internal fun Header(
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
TextButton(
|
||||
onClick = onFilterReset,
|
||||
@@ -178,24 +148,3 @@ internal fun Header(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun FilterBottomSheetPreview() {
|
||||
FilterBottomSheet(
|
||||
// state = rememberLogFilterState(),
|
||||
onDismissRequest = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun HeaderPreview(){
|
||||
Header(text = "Filter", isSelectedFilterEmpty = true, onFilterReset = {})
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun FilterContentPreview(){
|
||||
// FilterContent(state = rememberLogFilterState(), onDismissRequest = {})
|
||||
}
|
||||
+33
-37
@@ -9,48 +9,44 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
//import com.digitoolsolutions.android.storage.data.local.db.entity.Log
|
||||
//import com.digitoolsolutions.android.storage.model.LogFilter
|
||||
//import java.time.Instant
|
||||
//import java.time.ZoneId
|
||||
//import java.time.format.DateTimeFormatter
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Log
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
@Composable
|
||||
fun LogItem(/** log: Log,*/ modifier: Modifier = Modifier) {
|
||||
// val formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss")
|
||||
// .withZone(ZoneId.systemDefault())
|
||||
// val formattedTime = formatter.format(Instant.ofEpochMilli(log.timestamp))
|
||||
fun LogItem(log: Log, modifier: Modifier = Modifier) {
|
||||
val dateTime = Instant.fromEpochMilliseconds(log.timestamp)
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
val formattedTime = "${dateTime.date} ${dateTime.hour}:${dateTime.minute}:${dateTime.second}"
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
){
|
||||
// Column(
|
||||
// modifier = Modifier.padding(16.dp),
|
||||
// ) {
|
||||
// Text(
|
||||
// text = formattedTime,
|
||||
// style = MaterialTheme.typography.bodySmall
|
||||
// )
|
||||
// Text(
|
||||
// text = log.title,
|
||||
// style = MaterialTheme.typography.titleMedium
|
||||
// )
|
||||
// Text(
|
||||
// text = "${if(log.source == LogFilter.Device.toString()) log.deviceId ?: "Unknown" else log.source.replaceFirstChar{it.uppercase()}} - ${log.content}",
|
||||
// style = MaterialTheme.typography.bodyMedium,
|
||||
// color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
// )
|
||||
// }
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = formattedTime,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
text = log.title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
val sourceText = if (log.source == LogFilter.Device.name) {
|
||||
log.deviceId ?: "Unknown Device"
|
||||
} else {
|
||||
log.source
|
||||
}
|
||||
Text(
|
||||
text = "$sourceText - ${log.content}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
HorizontalDivider(thickness = DividerDefaults.Thickness, color = DividerDefaults.color)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun BondedDeviceItemPreview() {
|
||||
LogItem(
|
||||
// Log( 1, "Log 1", "This is a log"),
|
||||
)
|
||||
}
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
CREATE TABLE Device (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE Log (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
deviceId TEXT,
|
||||
source TEXT NOT NULL,
|
||||
FOREIGN KEY(deviceId) REFERENCES Device(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX log_device_id ON Log(deviceId);
|
||||
CREATE INDEX log_timestamp_id ON Log(timestamp, id);
|
||||
|
||||
-- Device Queries
|
||||
insertDevice:
|
||||
INSERT OR REPLACE INTO Device(id, name)
|
||||
VALUES (?, ?);
|
||||
|
||||
getAllDevices:
|
||||
SELECT * FROM Device;
|
||||
|
||||
getDevicesFirstPage:
|
||||
SELECT * FROM Device ORDER BY id ASC LIMIT :limit;
|
||||
|
||||
getDevicesNextPage:
|
||||
SELECT * FROM Device WHERE id > :lastId ORDER BY id ASC LIMIT :limit;
|
||||
|
||||
getDeviceById:
|
||||
SELECT * FROM Device WHERE id = ?;
|
||||
|
||||
deleteDeviceById:
|
||||
DELETE FROM Device WHERE id = ?;
|
||||
|
||||
-- Log Queries
|
||||
insertLog:
|
||||
INSERT INTO Log(title, content, timestamp, deviceId, source)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
|
||||
getAllLogs:
|
||||
SELECT * FROM Log ORDER BY timestamp DESC, id DESC;
|
||||
|
||||
getLogsFirstPage:
|
||||
SELECT * FROM Log ORDER BY timestamp DESC, id DESC LIMIT :limit;
|
||||
|
||||
getLogsNextPage:
|
||||
SELECT * FROM Log
|
||||
WHERE (timestamp < :lastTimestamp) OR (timestamp = :lastTimestamp AND id < :lastId)
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT :limit;
|
||||
|
||||
getLogsByTypeFirstPage:
|
||||
SELECT * FROM Log
|
||||
WHERE source = :source
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT :limit;
|
||||
|
||||
getLogsByTypeNextPage:
|
||||
SELECT * FROM Log
|
||||
WHERE source = :source AND ((timestamp < :lastTimestamp) OR (timestamp = :lastTimestamp AND id < :lastId))
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT :limit;
|
||||
|
||||
getLogsByDeviceFirstPage:
|
||||
SELECT * FROM Log
|
||||
WHERE deviceId = :deviceId
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT :limit;
|
||||
|
||||
getLogsByDeviceNextPage:
|
||||
SELECT * FROM Log
|
||||
WHERE deviceId = :deviceId AND ((timestamp < :lastTimestamp) OR (timestamp = :lastTimestamp AND id < :lastId))
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT :limit;
|
||||
|
||||
clearAllLogs:
|
||||
DELETE FROM Log;
|
||||
+10
-1
@@ -1,9 +1,18 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.di
|
||||
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.driver.native.NativeSqliteDriver
|
||||
import com.digitoolsolutions.app.torquevaultkmp.AppStoragePlatform
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase
|
||||
import org.koin.dsl.module
|
||||
|
||||
actual val platformModule = module {
|
||||
single<AppStorage> { AppStoragePlatform() }
|
||||
}
|
||||
single<SqlDriver> {
|
||||
NativeSqliteDriver(
|
||||
schema = AppDatabase.Schema,
|
||||
name = "app.db"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user