Implementation SQLDelight database
This commit is contained in:
@@ -7,4 +7,5 @@ plugins {
|
|||||||
alias(libs.plugins.composeCompiler) apply false
|
alias(libs.plugins.composeCompiler) apply false
|
||||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||||
alias(libs.plugins.kotlin.serialization) apply false
|
alias(libs.plugins.kotlin.serialization) apply false
|
||||||
|
alias(libs.plugins.sqldelight) apply false
|
||||||
}
|
}
|
||||||
@@ -7,14 +7,19 @@ plugins {
|
|||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
alias(libs.plugins.composeCompiler)
|
alias(libs.plugins.composeCompiler)
|
||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.sqldelight)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqldelight {
|
||||||
|
databases {
|
||||||
|
create("AppDatabase") {
|
||||||
|
packageName.set("com.digitoolsolutions.app.torquevaultkmp.data.storage.db")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
androidTarget {
|
androidTarget()
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(JvmTarget.JVM_11)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
iosArm64(),
|
iosArm64(),
|
||||||
@@ -22,7 +27,8 @@ kotlin {
|
|||||||
).forEach { iosTarget ->
|
).forEach { iosTarget ->
|
||||||
iosTarget.binaries.framework {
|
iosTarget.binaries.framework {
|
||||||
baseName = "ComposeApp"
|
baseName = "ComposeApp"
|
||||||
isStatic = true
|
isStatic = false
|
||||||
|
linkerOpts("-lsqlite3")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +42,7 @@ kotlin {
|
|||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.koin.compose.viewmodel)
|
implementation(libs.koin.compose.viewmodel)
|
||||||
implementation(libs.ktor.client.okhttp)
|
implementation(libs.ktor.client.okhttp)
|
||||||
|
implementation(libs.sqldelight.android)
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(libs.compose.runtime)
|
implementation(libs.compose.runtime)
|
||||||
@@ -56,10 +63,15 @@ kotlin {
|
|||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.compose.viewmodel)
|
implementation(libs.koin.compose.viewmodel)
|
||||||
|
|
||||||
|
implementation(libs.sqldelight.runtime)
|
||||||
|
implementation(libs.sqldelight.coroutines)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
||||||
implementation(libs.logger.napier)
|
implementation(libs.logger.napier)
|
||||||
}
|
}
|
||||||
iosMain.dependencies {
|
iosMain.dependencies {
|
||||||
implementation(libs.ktor.client.darwin)
|
implementation(libs.ktor.client.darwin)
|
||||||
|
implementation(libs.sqldelight.native)
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(libs.kotlin.test)
|
implementation(libs.kotlin.test)
|
||||||
|
|||||||
+11
-1
@@ -1,10 +1,20 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.di
|
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.AppStoragePlatform
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
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.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
actual val platformModule = module {
|
actual val platformModule = module {
|
||||||
single<AppStorage> { AppStoragePlatform(androidContext()) }
|
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)
|
config?.invoke(this)
|
||||||
modules(
|
modules(
|
||||||
appModule,
|
appModule,
|
||||||
|
storageModule,
|
||||||
networkModule,
|
networkModule,
|
||||||
viewModelModule,
|
viewModelModule,
|
||||||
platformModule
|
platformModule
|
||||||
|
|||||||
+14
@@ -4,12 +4,17 @@ import com.digitoolsolutions.app.torquevaultkmp.AppViewModel
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.createPlatformHttpClient
|
import com.digitoolsolutions.app.torquevaultkmp.createPlatformHttpClient
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.AuthApi
|
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.AuthApi
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.WorkOrderApi
|
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.WorkOrderRepository
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderRepositoryImpl
|
import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderRepositoryImpl
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
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.domain.usecase.FetchWorkOrdersUseCase
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.auth.AuthViewModel
|
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.home.HomeViewModel
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogViewModel
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingViewModel
|
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingViewModel
|
||||||
import org.koin.core.module.Module
|
import org.koin.core.module.Module
|
||||||
import org.koin.core.module.dsl.factoryOf
|
import org.koin.core.module.dsl.factoryOf
|
||||||
@@ -24,6 +29,13 @@ val appModule = module {
|
|||||||
single { TokenManager(get()) }
|
single { TokenManager(get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val storageModule = module {
|
||||||
|
single { AppDatabase(get()) }
|
||||||
|
single { get<AppDatabase>().appDatabaseQueries }
|
||||||
|
single { LogRepository(get()) }
|
||||||
|
single { DeviceRepository(get()) }
|
||||||
|
}
|
||||||
|
|
||||||
val networkModule = module {
|
val networkModule = module {
|
||||||
// Auth client (no interceptor to avoid circular dependency and recursion)
|
// Auth client (no interceptor to avoid circular dependency and recursion)
|
||||||
single(named("authClient")) { createPlatformHttpClient(get(), null) }
|
single(named("authClient")) { createPlatformHttpClient(get(), null) }
|
||||||
@@ -44,4 +56,6 @@ val viewModelModule = module {
|
|||||||
factory { AuthViewModel(get(), get()) }
|
factory { AuthViewModel(get(), get()) }
|
||||||
singleOf(::AppViewModel)
|
singleOf(::AppViewModel)
|
||||||
factoryOf(::SettingViewModel)
|
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
|
package com.digitoolsolutions.app.torquevaultkmp.screens.bonded
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
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.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
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.PullToRefreshBox
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||||
import androidx.compose.runtime.Composable
|
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.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.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Device
|
||||||
import kotlinx.coroutines.launch
|
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.jetbrains.compose.resources.stringResource
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.bonded_title
|
import torquevaultkmp.composeapp.generated.resources.bonded_title
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BondedScreen(
|
fun BondedScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
// bleScannerViewModel: ScannerViewModel,
|
viewModel: BondedViewModel = koinViewModel()
|
||||||
// viewModel: BondedViewModel = hiltViewModel()
|
|
||||||
) {
|
) {
|
||||||
// val devices = viewModel.bondedDevices.collectAsLazyPagingItems()
|
val devices by viewModel.bondedDevices.collectAsState()
|
||||||
|
val isLoading by viewModel.isLoading.collectAsState()
|
||||||
|
|
||||||
BondedContent(
|
BondedContent(
|
||||||
// devices = devices,
|
devices = devices,
|
||||||
// onRefresh = {devices.refresh()},
|
isLoading = isLoading,
|
||||||
// onDelete = {
|
onAddFake = { viewModel.addFakeDevice() },
|
||||||
// viewModel.removeDevice(it)
|
onDelete = { viewModel.removeDevice(it.id) },
|
||||||
// bleScannerViewModel.removeBondedCache(it.id)
|
onLoadMore = { viewModel.loadNextPage() },
|
||||||
// }
|
onRefresh = { viewModel.refresh() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun BondedContent(
|
internal fun BondedContent(
|
||||||
// devices: LazyPagingItems<Device>,
|
devices: List<Device>,
|
||||||
// onRefresh: () -> Unit,
|
isLoading: Boolean,
|
||||||
// onDelete: (Device) -> Unit
|
onAddFake: () -> Unit,
|
||||||
|
onDelete: (Device) -> Unit,
|
||||||
|
onLoadMore: () -> Unit,
|
||||||
|
onRefresh: () -> Unit
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
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 (
|
Scaffold (
|
||||||
modifier = Modifier.imePadding(),
|
modifier = Modifier.imePadding(),
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -59,50 +98,41 @@ internal fun BondedContent(
|
|||||||
title = {Text(stringResource(Res.string.bonded_title))},
|
title = {Text(stringResource(Res.string.bonded_title))},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingActionButton(onClick = onAddFake) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = "Add Fake Device")
|
||||||
|
}
|
||||||
|
},
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) })
|
snackbarHost = { SnackbarHost(snackbarHostState) })
|
||||||
{ padding ->
|
{ padding ->
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = false,
|
isRefreshing = isLoading && devices.isEmpty(),
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
// onRefresh()
|
onRefresh()
|
||||||
scope.launch {
|
|
||||||
pullToRefreshState.animateToHidden()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
state = pullToRefreshState,
|
state = pullToRefreshState,
|
||||||
modifier = Modifier.fillMaxSize().imePadding().padding(padding).padding(horizontal = 16.dp, vertical = 12.dp),
|
modifier = Modifier.fillMaxSize().imePadding().padding(padding).padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
){
|
){
|
||||||
// if (devices.itemCount == 0) {
|
if (devices.isEmpty() && !isLoading) {
|
||||||
// EmptyBondedView()
|
EmptyBondedView()
|
||||||
// }else {
|
} else {
|
||||||
// LazyColumn{
|
LazyColumn(state = listState) {
|
||||||
// items(count = devices.itemCount, key = { index -> devices[index]?.id ?: index }) { index ->
|
items(items = devices, key = { it.id }) { device ->
|
||||||
// val device = devices[index]
|
BondedDeviceItem(device, onDelete = { onDelete(it) })
|
||||||
// device?.let { it ->
|
}
|
||||||
// BondedDeviceItem(it, 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.Text
|
||||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
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.ui.draw.clip
|
||||||
@@ -32,13 +33,22 @@ import androidx.compose.ui.graphics.lerp
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Device
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BondedDeviceItem(/** device: Device, onDelete: (Device) -> Unit,*/ modifier: Modifier = Modifier) {
|
fun BondedDeviceItem(device: Device, onDelete: (Device) -> Unit, modifier: Modifier = Modifier) {
|
||||||
val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
|
val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
|
||||||
SwipeToDismissBoxValue.Settled,
|
SwipeToDismissBoxValue.Settled,
|
||||||
SwipeToDismissBoxDefaults.positionalThreshold
|
SwipeToDismissBoxDefaults.positionalThreshold
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (swipeToDismissBoxState.currentValue == SwipeToDismissBoxValue.EndToStart) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
onDelete(device)
|
||||||
|
swipeToDismissBoxState.snapTo(SwipeToDismissBoxValue.Settled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SwipeToDismissBox(
|
SwipeToDismissBox(
|
||||||
state = swipeToDismissBoxState,
|
state = swipeToDismissBoxState,
|
||||||
modifier = modifier.fillMaxSize().padding(vertical = 4.dp),
|
modifier = modifier.fillMaxSize().padding(vertical = 4.dp),
|
||||||
@@ -46,13 +56,14 @@ fun BondedDeviceItem(/** device: Device, onDelete: (Device) -> Unit,*/ modifier:
|
|||||||
backgroundContent = {
|
backgroundContent = {
|
||||||
when (swipeToDismissBoxState.dismissDirection) {
|
when (swipeToDismissBoxState.dismissDirection) {
|
||||||
SwipeToDismissBoxValue.StartToEnd -> {
|
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 -> {}
|
SwipeToDismissBoxValue.Settled -> {}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
// onDismiss = {onDelete(device)},
|
|
||||||
) {
|
) {
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -65,15 +76,15 @@ fun BondedDeviceItem(/** device: Device, onDelete: (Device) -> Unit,*/ modifier:
|
|||||||
CircularIcon( imageVector = Icons.Default.Bluetooth, backgroundColor = Color(0xFF0082FC))
|
CircularIcon( imageVector = Icons.Default.Bluetooth, backgroundColor = Color(0xFF0082FC))
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Column {
|
Column {
|
||||||
// Text(
|
Text(
|
||||||
// text = device.name,
|
text = device.name,
|
||||||
// style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
// )
|
)
|
||||||
// Text(
|
Text(
|
||||||
// text = device.id,
|
text = device.id,
|
||||||
// style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
// color = MaterialTheme.colorScheme.onSurfaceVariant
|
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
|
package com.digitoolsolutions.app.torquevaultkmp.screens.logs
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
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.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.DeleteForever
|
import androidx.compose.material.icons.filled.DeleteForever
|
||||||
import androidx.compose.material.icons.outlined.FilterList
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
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.PullToRefreshBox
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||||
import androidx.compose.runtime.Composable
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.FilterBottomSheet
|
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
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.jetbrains.compose.resources.stringResource
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.log_title
|
import torquevaultkmp.composeapp.generated.resources.log_title
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LogScreen(
|
fun LogScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
// viewModel: LogViewModel = hiltViewModel()
|
viewModel: LogViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
// val state = rememberLogFilterState()
|
val logs by viewModel.logs.collectAsState()
|
||||||
// val logs = viewModel.getLogs(filter = state.activeFilter).collectAsLazyPagingItems()
|
val activeFilter by viewModel.activeFilter.collectAsState()
|
||||||
|
val isLoading by viewModel.isLoading.collectAsState()
|
||||||
|
|
||||||
LogContent(
|
LogContent(
|
||||||
// state = state,
|
logs = logs,
|
||||||
// logs = logs,
|
activeFilter = activeFilter,
|
||||||
// onRefresh = {logs.refresh()},
|
isLoading = isLoading,
|
||||||
// onDeleteAllLogs = {viewModel.removeAllLog()},
|
onFilterSelected = { viewModel.setFilter(it) },
|
||||||
|
onAddFakeLog = { viewModel.addFakeLog() },
|
||||||
|
onDeleteAllLogs = { viewModel.clearAllLogs() },
|
||||||
|
onLoadMore = { viewModel.loadNextPage() },
|
||||||
|
onRefresh = { viewModel.refresh() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LogContent(
|
internal fun LogContent(
|
||||||
// state: LogFilterState,
|
logs: List<Log>,
|
||||||
// logs: LazyPagingItems<Log>,
|
activeFilter: LogFilter,
|
||||||
// onRefresh: () -> Unit,
|
isLoading: Boolean,
|
||||||
// onDeleteAllLogs: () -> Unit,
|
onFilterSelected: (LogFilter) -> Unit,
|
||||||
|
onAddFakeLog: () -> Unit,
|
||||||
|
onDeleteAllLogs: () -> Unit,
|
||||||
|
onLoadMore: () -> Unit,
|
||||||
|
onRefresh: () -> Unit
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||||
var showBottomSheet 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 (
|
Scaffold (
|
||||||
modifier = Modifier.imePadding(),
|
modifier = Modifier.imePadding(),
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -82,23 +126,25 @@ internal fun LogContent(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.FilterList,
|
Icons.Outlined.FilterList,
|
||||||
contentDescription = "Filter logs",
|
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) })
|
snackbarHost = { SnackbarHost(snackbarHostState) })
|
||||||
{ padding ->
|
{ padding ->
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = false,
|
isRefreshing = isLoading && logs.isEmpty(),
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
// onRefresh()
|
onRefresh()
|
||||||
scope.launch {
|
|
||||||
pullToRefreshState.animateToHidden()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
state = pullToRefreshState,
|
state = pullToRefreshState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -107,18 +153,25 @@ internal fun LogContent(
|
|||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
){
|
){
|
||||||
// if (logs.itemCount == 0) {
|
if (logs.isEmpty() && !isLoading) {
|
||||||
// EmptyLogView()
|
EmptyLogView()
|
||||||
// }else {
|
} else {
|
||||||
// LazyColumn{
|
LazyColumn(state = listState) {
|
||||||
// items(logs.itemCount, key = { index -> logs[index]?.id ?: index }) { index ->
|
items(items = logs, key = { it.id }) { log ->
|
||||||
// val log = logs[index]
|
LogItem(log)
|
||||||
// log?.let { it ->
|
}
|
||||||
// LogItem(it)
|
if (isLoading && logs.isNotEmpty()) {
|
||||||
// }
|
item {
|
||||||
// }
|
Box(
|
||||||
// }
|
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||||
// }
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (showConfirmDialog) {
|
if (showConfirmDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
@@ -127,7 +180,7 @@ internal fun LogContent(
|
|||||||
text = { Text("Warning: This will permanently delete all current logs. This action cannot be undone.") },
|
text = { Text("Warning: This will permanently delete all current logs. This action cannot be undone.") },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
// onDeleteAllLogs()
|
onDeleteAllLogs()
|
||||||
showConfirmDialog = false
|
showConfirmDialog = false
|
||||||
}) {
|
}) {
|
||||||
Text("Delete", color = Color.Red)
|
Text("Delete", color = Color.Red)
|
||||||
@@ -135,35 +188,17 @@ internal fun LogContent(
|
|||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showConfirmDialog = false }) {
|
TextButton(onClick = { showConfirmDialog = false }) {
|
||||||
Text("Cancel", color = Color.White)
|
Text("Cancel", color = Color.Gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
FilterBottomSheet(
|
FilterBottomSheet(
|
||||||
// state = state,
|
activeFilter = activeFilter,
|
||||||
|
onFilterSelected = onFilterSelected,
|
||||||
onDismissRequest = {showBottomSheet = false}
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
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.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.clear
|
import torquevaultkmp.composeapp.generated.resources.clear
|
||||||
@@ -35,13 +35,12 @@ import torquevaultkmp.composeapp.generated.resources.clear
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FilterBottomSheet(
|
fun FilterBottomSheet(
|
||||||
// state: LogFilterState,
|
activeFilter: LogFilter,
|
||||||
|
onFilterSelected: (LogFilter) -> Unit,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = onDismissRequest,
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
tonalElevation = 16.dp,
|
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
|
@Composable
|
||||||
internal fun FilterContent(
|
internal fun FilterContent(
|
||||||
// state: LogFilterState,
|
activeFilter: LogFilter,
|
||||||
|
onFilterSelected: (LogFilter) -> Unit,
|
||||||
|
onClear: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier.padding(bottom = 32.dp),
|
||||||
) {
|
) {
|
||||||
Header(
|
Header(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
text = "Filter",
|
text = "Filter",
|
||||||
isSelectedFilterEmpty = true ,// state.isEmpty,
|
isSelectedFilterEmpty = activeFilter == LogFilter.All,
|
||||||
onFilterReset = {
|
onFilterReset = onClear
|
||||||
// state.clear()
|
|
||||||
onDismissRequest()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// if (state.filters.isNotEmpty()) {
|
LogFilter.entries.forEach { filter ->
|
||||||
// FlowRow(
|
Row(
|
||||||
// modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier
|
||||||
// horizontalArrangement = Arrangement.spacedBy(8.dp),
|
.fillMaxWidth()
|
||||||
// verticalArrangement = Arrangement.spacedBy(0.dp),
|
.selectable(
|
||||||
// ) {
|
selected = activeFilter == filter,
|
||||||
// state.filters.forEach { filter ->
|
onClick = { onFilterSelected(filter) },
|
||||||
// Row(
|
role = Role.RadioButton
|
||||||
// modifier = Modifier
|
)
|
||||||
// .clip(RoundedCornerShape(8.dp))
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
// .selectable(
|
verticalAlignment = Alignment.CenterVertically
|
||||||
// selected = state.activeFilter == filter,
|
) {
|
||||||
// onClick = { state.setFilter(filter) },
|
RadioButton(
|
||||||
// role = Role.RadioButton
|
selected = activeFilter == filter,
|
||||||
// )
|
onClick = { onFilterSelected(filter) }
|
||||||
// .padding(end = 16.dp),
|
)
|
||||||
// verticalAlignment = Alignment.CenterVertically
|
Text(
|
||||||
// ) {
|
text = filter.name,
|
||||||
// RadioButton(
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
// selected = state.activeFilter == filter,
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
// 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() })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +127,7 @@ internal fun Header(
|
|||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier
|
modifier = Modifier.weight(1f)
|
||||||
.weight(1f)
|
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onFilterReset,
|
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.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
//import com.digitoolsolutions.android.storage.data.local.db.entity.Log
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Log
|
||||||
//import com.digitoolsolutions.android.storage.model.LogFilter
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
|
||||||
//import java.time.Instant
|
import kotlin.time.Instant
|
||||||
//import java.time.ZoneId
|
import kotlinx.datetime.TimeZone
|
||||||
//import java.time.format.DateTimeFormatter
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogItem(/** log: Log,*/ modifier: Modifier = Modifier) {
|
fun LogItem(log: Log, modifier: Modifier = Modifier) {
|
||||||
// val formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss")
|
val dateTime = Instant.fromEpochMilliseconds(log.timestamp)
|
||||||
// .withZone(ZoneId.systemDefault())
|
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
// val formattedTime = formatter.format(Instant.ofEpochMilli(log.timestamp))
|
val formattedTime = "${dateTime.date} ${dateTime.hour}:${dateTime.minute}:${dateTime.second}"
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
){
|
){
|
||||||
// Column(
|
Column(
|
||||||
// modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
// ) {
|
) {
|
||||||
// Text(
|
Text(
|
||||||
// text = formattedTime,
|
text = formattedTime,
|
||||||
// style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
// )
|
)
|
||||||
// Text(
|
Text(
|
||||||
// text = log.title,
|
text = log.title,
|
||||||
// style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
// )
|
)
|
||||||
// Text(
|
val sourceText = if (log.source == LogFilter.Device.name) {
|
||||||
// text = "${if(log.source == LogFilter.Device.toString()) log.deviceId ?: "Unknown" else log.source.replaceFirstChar{it.uppercase()}} - ${log.content}",
|
log.deviceId ?: "Unknown Device"
|
||||||
// style = MaterialTheme.typography.bodyMedium,
|
} else {
|
||||||
// color = MaterialTheme.colorScheme.onSurfaceVariant
|
log.source
|
||||||
// )
|
}
|
||||||
// }
|
Text(
|
||||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
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
|
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.AppStoragePlatform
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
actual val platformModule = module {
|
actual val platformModule = module {
|
||||||
single<AppStorage> { AppStoragePlatform() }
|
single<AppStorage> { AppStoragePlatform() }
|
||||||
}
|
single<SqlDriver> {
|
||||||
|
NativeSqliteDriver(
|
||||||
|
schema = AppDatabase.Schema,
|
||||||
|
name = "app.db"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ androidx-core = "1.18.0"
|
|||||||
androidx-espresso = "3.7.0"
|
androidx-espresso = "3.7.0"
|
||||||
androidx-lifecycle = "2.10.0"
|
androidx-lifecycle = "2.10.0"
|
||||||
androidx-testExt = "1.3.0"
|
androidx-testExt = "1.3.0"
|
||||||
composeMultiplatform = "1.10.3"
|
composeMultiplatform = "1.11.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.3.21"
|
kotlin = "2.3.21"
|
||||||
material3 = "1.10.0-alpha05"
|
material3 = "1.10.0-alpha05"
|
||||||
@@ -18,6 +18,8 @@ ktor = "3.4.3"
|
|||||||
material-icons-extended = "1.7.3"
|
material-icons-extended = "1.7.3"
|
||||||
navigation-compose = "2.9.2"
|
navigation-compose = "2.9.2"
|
||||||
logger = "2.7.1"
|
logger = "2.7.1"
|
||||||
|
sqldelight = "2.3.2"
|
||||||
|
kotlinx-datetime = "0.8.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
@@ -58,6 +60,13 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx
|
|||||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||||
logger-napier = { module = "io.github.aakira:napier", version.ref = "logger" }
|
logger-napier = { module = "io.github.aakira:napier", version.ref = "logger" }
|
||||||
|
|
||||||
|
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" }
|
||||||
|
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
|
sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
|
sqldelight-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
|
||||||
|
|
||||||
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||||
@@ -65,6 +74,7 @@ composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMul
|
|||||||
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
|
||||||
|
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
ktor = [
|
ktor = [
|
||||||
|
|||||||
Reference in New Issue
Block a user