feat: [DATABASE] queryview (#325)

Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
Florent CHAMPIGNY 2025-10-09 21:55:34 +02:00 committed by GitHub
parent f727a974e8
commit 0b60acfde1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 188 additions and 20 deletions

View file

@ -5,19 +5,26 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.common.combines
import io.github.openflocon.domain.database.usecase.ExecuteDatabaseQueryUseCase
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.database.mapper.toUi
import io.github.openflocon.flocondesktop.features.database.model.DatabaseScreenState
import io.github.openflocon.flocondesktop.features.database.model.QueryResultUiModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicReference
import kotlin.time.Duration.Companion.seconds
class DatabaseTabViewModel(
private val params: Params,
@ -34,6 +41,32 @@ class DatabaseTabViewModel(
var query = mutableStateOf("")
private val autoUpdateJob = AtomicReference<Job?>(null)
data class AutoUpdate(
val query: String? = null,
val isEnabled: Boolean = false,
)
private val isVisible = MutableStateFlow(false)
fun onVisible() {
isVisible.update { true }
}
fun onNotVisible() {
isVisible.update { false }
}
private val _autoUpdate = MutableStateFlow(AutoUpdate())
val isAutoUpdateEnabled = _autoUpdate
.map { it.isEnabled }
.flowOn(dispatcherProvider.viewModel)
.stateIn(
scope = viewModelScope,
started = SharingStarted.Companion.WhileSubscribed(5000),
initialValue = false,
)
private val queryResult = MutableStateFlow<QueryResultUiModel?>(null)
val state: StateFlow<DatabaseScreenState> = queryResult.map { queryResult ->
@ -48,12 +81,15 @@ class DatabaseTabViewModel(
init {
params.tableName?.let {
updateQuery(buildString {
val query = buildString {
appendLine("SELECT * ")
appendLine("FROM $it")
append("LIMIT 50 OFFSET 0")
})
executeQuery()
}
updateQuery(query)
viewModelScope.launch(dispatcherProvider.viewModel) {
executeQuery(query = query, editAutoUpdate = true)
}
}
}
@ -62,25 +98,79 @@ class DatabaseTabViewModel(
}
fun executeQuery() {
executeQuery(query = query.value)
viewModelScope.launch(dispatcherProvider.viewModel) {
executeQuery(query = query.value, editAutoUpdate = true)
}
}
private fun executeQuery(query: String) {
init {
viewModelScope.launch(dispatcherProvider.viewModel) {
executeDatabaseQueryUseCase(
query = query,
databaseId = params.databaseId,
).fold(doOnSuccess = {
queryResult.value = it.toUi()
}, doOnFailure = {
feedbackDisplayer.displayMessage("database failure : $it")
})
combines(isVisible, _autoUpdate)
.distinctUntilChanged()
.collect { (isVisible, autoUpdate) ->
refreshAutoUpdate(
isVisible = isVisible,
autoUpdate = autoUpdate,
)
}
}
}
private suspend fun executeQuery(query: String, editAutoUpdate: Boolean) {
println("executeQuery: $query")
executeDatabaseQueryUseCase(
query = query,
databaseId = params.databaseId,
).fold(doOnSuccess = {
queryResult.value = it.toUi()
if (editAutoUpdate) {
_autoUpdate.update {
it.copy(
query = query,
)
}
}
}, doOnFailure = {
feedbackDisplayer.displayMessage("database failure : $it")
})
}
fun clearQuery() {
viewModelScope.launch(dispatcherProvider.viewModel) {
updateQuery("")
queryResult.update { null }
_autoUpdate.update {
it.copy(
query = null,
isEnabled = false,
)
}
}
}
fun updateAutoUpdate(value: Boolean) {
_autoUpdate.update {
it.copy(
isEnabled = value,
)
}
}
private fun refreshAutoUpdate(isVisible: Boolean, autoUpdate: AutoUpdate) {
val job = autoUpdateJob.get()
if (!autoUpdate.isEnabled || !isVisible) {
job?.cancel()
return
} else {
job?.cancel()
val autoUpdateJob = viewModelScope.launch(dispatcherProvider.viewModel) {
while (isActive) {
delay(3.seconds)
val query = _autoUpdate.value.takeIf { it.isEnabled }?.query ?: return@launch
executeQuery(query, editAutoUpdate = false)
}
}
this.autoUpdateJob.set(autoUpdateJob)
}
}

View file

@ -1,16 +1,30 @@
package io.github.openflocon.flocondesktop.features.database.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isMetaPressed
@ -27,15 +41,17 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
fun DatabaseQueryView(
query: String,
autoUpdate: Boolean,
updateQuery: (query: String) -> Unit,
executeQuery: (query: String) -> Unit,
updateAutoUpdate: (value: Boolean) -> Unit,
clearQuery: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(
color = FloconTheme.colorPalette.secondary,
color = FloconTheme.colorPalette.primary,
shape = FloconTheme.shapes.medium
)
) {
@ -69,21 +85,66 @@ fun DatabaseQueryView(
) {
FloconButton(
onClick = { executeQuery(query) },
containerColor = FloconTheme.colorPalette.tertiary,
modifier = Modifier
.padding(all = 8.dp)
) {
Text("Run", modifier = Modifier.padding(horizontal = 10.dp))
val contentColor = FloconTheme.colorPalette.onTertiary
Row(
Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
Icons.Filled.PlayArrow,
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = ColorFilter.tint(contentColor)
)
Text("Run Query", color = contentColor)
}
}
Row(
Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = autoUpdate,
onCheckedChange = {
updateAutoUpdate(it)
}, colors = CheckboxDefaults.colors(
uncheckedColor = FloconTheme.colorPalette.secondary,
checkedColor = FloconTheme.colorPalette.secondary,
)
)
Text(
"Auto Update",
color = FloconTheme.colorPalette.onPrimary,
style = FloconTheme.typography.bodySmall
)
}
Spacer(modifier = Modifier.weight(1f))
FloconButton(
onClick = clearQuery,
modifier = Modifier
.padding(all = 8.dp)
Box(
modifier = Modifier.clip(RoundedCornerShape(2.dp)).clickable {
clearQuery()
}.size(40.dp),
contentAlignment = Alignment.Center
) {
Text("Reset", modifier = Modifier.padding(horizontal = 10.dp))
Image(
Icons.Filled.Delete,
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = ColorFilter.tint(FloconTheme.colorPalette.onPrimary)
)
}
Spacer(modifier = Modifier.width(2.dp))
}
}
}
@ -98,6 +159,8 @@ private fun DatabaseQueryViewPreview() {
modifier = Modifier.fillMaxWidth(),
query = "SELECT * FROM TABLE_NAME",
updateQuery = {},
autoUpdate = true,
updateAutoUpdate = {},
)
}
}

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -32,12 +33,22 @@ fun DatabaseTabView(
parameters = { parametersOf(params) }
)
DisposableEffect(viewModel) {
viewModel.onVisible()
onDispose {
viewModel.onNotVisible()
}
}
val state: DatabaseScreenState by viewModel.state.collectAsStateWithLifecycle()
val autoUpdate by viewModel.isAutoUpdateEnabled.collectAsStateWithLifecycle()
DatabaseTabViewContent(
query = viewModel.query.value,
autoUpdate = autoUpdate,
updateQuery = viewModel::updateQuery,
executeQuery = viewModel::executeQuery,
clearQuery = viewModel::clearQuery,
updateAutoUpdate = viewModel::updateAutoUpdate,
state = state,
)
}
@ -45,9 +56,11 @@ fun DatabaseTabView(
@Composable
private fun DatabaseTabViewContent(
query: String,
autoUpdate: Boolean,
updateQuery: (String) -> Unit,
executeQuery: () -> Unit,
clearQuery: () -> Unit,
updateAutoUpdate: (Boolean) -> Unit,
state: DatabaseScreenState,
) {
Column(
@ -60,9 +73,11 @@ private fun DatabaseTabViewContent(
DatabaseQueryView(
query = query,
updateQuery = updateQuery,
autoUpdate = autoUpdate,
executeQuery = {
executeQuery()
},
updateAutoUpdate = updateAutoUpdate,
clearQuery = clearQuery,
modifier = Modifier
.fillMaxWidth()