Implementation SQLDelight database

This commit is contained in:
2026-05-20 13:56:42 +07:00
parent 28901df627
commit 66fe3be32f
19 changed files with 710 additions and 363 deletions
+1
View File
@@ -7,4 +7,5 @@ plugins {
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.sqldelight) apply false
}
+18 -6
View File
@@ -7,14 +7,19 @@ plugins {
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.sqldelight)
}
sqldelight {
databases {
create("AppDatabase") {
packageName.set("com.digitoolsolutions.app.torquevaultkmp.data.storage.db")
}
}
}
kotlin {
androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
androidTarget()
listOf(
iosArm64(),
@@ -22,7 +27,8 @@ kotlin {
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
isStatic = false
linkerOpts("-lsqlite3")
}
}
@@ -36,6 +42,7 @@ kotlin {
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.ktor.client.okhttp)
implementation(libs.sqldelight.android)
}
commonMain.dependencies {
implementation(libs.compose.runtime)
@@ -56,10 +63,15 @@ kotlin {
implementation(libs.koin.core)
implementation(libs.koin.compose.viewmodel)
implementation(libs.sqldelight.runtime)
implementation(libs.sqldelight.coroutines)
implementation(libs.kotlinx.datetime)
implementation(libs.logger.napier)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
implementation(libs.sqldelight.native)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
@@ -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"
)
}
}
@@ -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)
}
@@ -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
@@ -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)
}
@@ -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
}
}
}
@@ -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 = {}
// )
}
@@ -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 }
}
}
}
@@ -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 = {}
)
}
@@ -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
// }
// }
//}
@@ -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 = {}
)
}
@@ -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
}
}
}
@@ -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 = {})
}
@@ -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"),
)
}
@@ -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;
@@ -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"
)
}
}
+11 -1
View File
@@ -9,7 +9,7 @@ androidx-core = "1.18.0"
androidx-espresso = "3.7.0"
androidx-lifecycle = "2.10.0"
androidx-testExt = "1.3.0"
composeMultiplatform = "1.10.3"
composeMultiplatform = "1.11.0"
junit = "4.13.2"
kotlin = "2.3.21"
material3 = "1.10.0-alpha05"
@@ -18,6 +18,8 @@ ktor = "3.4.3"
material-icons-extended = "1.7.3"
navigation-compose = "2.9.2"
logger = "2.7.1"
sqldelight = "2.3.2"
kotlinx-datetime = "0.8.0"
[libraries]
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" }
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]
androidApplication = { id = "com.android.application", 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" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
[bundles]
ktor = [