diff --git a/README.md b/README.md index dc0ae4fb..948dc58d 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ enableKick(false) ### Wizard -Use [Integration Wizard](content/wizard/index.html) to generate ready-to-paste plugin configuration and initialization snippets for selected modules and platforms. +Use [Integration Wizard](https://bartwell.github.io/kick/index.html) to generate ready-to-paste plugin configuration and initialization snippets for selected modules and platforms. For full setup details (manual integration, advanced configuration, shortcuts, launching, and full module docs), see [Advanced](content/docs/Advanced.md). diff --git a/content/docs/Advanced.md b/content/docs/Advanced.md index a31fb33c..1a9a77f8 100644 --- a/content/docs/Advanced.md +++ b/content/docs/Advanced.md @@ -429,6 +429,17 @@ Kick.controlPanel.getString("endpoint") Kick.controlPanel.getString("list") ``` +You can also set values programmatically via `Kick.controlPanel.set*()` methods: + +```kotlin +Kick.controlPanel.setBoolean("featureEnabled", true) +Kick.controlPanel.setInt("maxItems", 8) +Kick.controlPanel.setLong("timeoutMs", 15_000L) +Kick.controlPanel.setFloat("ratio", 0.75f) +Kick.controlPanel.setDouble("threshold", 0.95) +Kick.controlPanel.setString("endpoint", "https://staging.example.com") +``` + #### Actions You can also add action buttons to trigger code in your app. Collect control panel events and handle button IDs you defined in `ControlPanelItem(type = ActionType.Button("id"))`: @@ -646,4 +657,3 @@ class AppDelegate: NSObject, UIApplicationDelegate { ``` Desktop (Windows/macOS/Linux): when supported by the OS, Kick also adds a System Tray icon with the label "Inspect with Kick". Clicking the tray icon launches the viewer. The icon is removed automatically when the host app exits. This tray shortcut respects the same `enableShortcut` flag — set it to `false` to disable the icon. - diff --git a/module/logging/logging/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/FakeLogViewerComponent.kt b/module/logging/logging/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/FakeLogViewerComponent.kt index 4f054aea..04f29ef1 100644 --- a/module/logging/logging/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/FakeLogViewerComponent.kt +++ b/module/logging/logging/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/FakeLogViewerComponent.kt @@ -8,7 +8,7 @@ import ru.bartwell.kick.module.logging.core.persist.LogEntity internal class FakeLogViewerComponent( initial: List, ) : LogViewerComponent { - private val allLogs = initial.sortedByDescending { it.time } + private val allLogs = initial.sortedBy { it.time } private val regex = Regex("\\[(.*?)]") private val _model = MutableValue( LogViewerState( @@ -30,6 +30,10 @@ internal class FakeLogViewerComponent( override fun onBackPressed() = Unit + override fun onAutoScrollToggleClick() { + _model.value = model.value.copy(isAutoScrollEnabled = !model.value.isAutoScrollEnabled) + } + override fun onClearAllClick() { _model.value = model.value.copy(log = emptyList()) } diff --git a/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/DefaultLogViewerComponent.kt b/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/DefaultLogViewerComponent.kt index f5fd04fa..b3eff6d0 100644 --- a/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/DefaultLogViewerComponent.kt +++ b/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/DefaultLogViewerComponent.kt @@ -36,6 +36,10 @@ internal class DefaultLogViewerComponent( override fun onBackPressed() = onFinished() + override fun onAutoScrollToggleClick() { + _model.value = model.value.copy(isAutoScrollEnabled = !model.value.isAutoScrollEnabled) + } + override fun onClearAllClick() { uiScope.launch { database.getLogDao().deleteAll() } } diff --git a/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerComponent.kt b/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerComponent.kt index 6e2b49a6..42f5ea01 100644 --- a/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerComponent.kt +++ b/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerComponent.kt @@ -8,6 +8,7 @@ public interface LogViewerComponent : Component { public val model: Value public fun onBackPressed() + public fun onAutoScrollToggleClick() public fun onClearAllClick() public fun onFilterClick() public fun onFilterDialogDismiss() diff --git a/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerContent.kt b/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerContent.kt index bfca4756..1cda333d 100644 --- a/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerContent.kt +++ b/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerContent.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ClearAll import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.FileDownload +import androidx.compose.material.icons.filled.FileDownloadOff import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.FilterListOff import androidx.compose.material.icons.filled.Share @@ -24,6 +26,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -47,6 +50,13 @@ internal fun LogViewerContent( ) { val state by component.model.subscribeAsState() val context = platformContext() + val listState = rememberLazyListState() + + LaunchedEffect(state.isAutoScrollEnabled, state.log.lastOrNull()?.id) { + if (state.isAutoScrollEnabled && state.log.isNotEmpty()) { + listState.animateScrollToItem(state.log.lastIndex) + } + } Column(modifier = modifier) { TopAppBar( @@ -63,6 +73,17 @@ internal fun LogViewerContent( } Icon(imageVector = icon, contentDescription = description) } + IconButton( + onClick = component::onAutoScrollToggleClick, + modifier = Modifier.testTag("auto_scroll_toggle") + ) { + val (icon, description) = if (state.isAutoScrollEnabled) { + Icons.Filled.FileDownload to "Disable auto-scroll" + } else { + Icons.Filled.FileDownloadOff to "Enable auto-scroll" + } + Icon(imageVector = icon, contentDescription = description) + } IconButton(onClick = component::onClearAllClick, modifier = Modifier.testTag("clear_all")) { Icon(imageVector = Icons.Default.ClearAll, contentDescription = "Clear all") } @@ -86,7 +107,7 @@ internal fun LogViewerContent( } ErrorBox(modifier = Modifier.fillMaxSize(), error = state.error) { LazyColumn( - state = rememberLazyListState(), + state = listState, modifier = Modifier.fillMaxSize().testTag("log_list"), ) { items(state.log) { item -> diff --git a/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerState.kt b/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerState.kt index 6b2e69ce..db5f3204 100644 --- a/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerState.kt +++ b/module/logging/logging/src/commonMain/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerState.kt @@ -5,6 +5,7 @@ import ru.bartwell.kick.module.logging.core.persist.LogEntity public data class LogViewerState( val log: List = emptyList(), val error: String? = null, + val isAutoScrollEnabled: Boolean = true, val filterQuery: String = "", val isFilterActive: Boolean = false, val isFilterDialogVisible: Boolean = false, diff --git a/module/logging/logging/src/commonMain/sqldelight/ru/bartwell/kick/module/logging/db/log.sq b/module/logging/logging/src/commonMain/sqldelight/ru/bartwell/kick/module/logging/db/log.sq index 65631bed..ec6133dc 100644 --- a/module/logging/logging/src/commonMain/sqldelight/ru/bartwell/kick/module/logging/db/log.sq +++ b/module/logging/logging/src/commonMain/sqldelight/ru/bartwell/kick/module/logging/db/log.sq @@ -7,13 +7,13 @@ CREATE TABLE log ( message TEXT NOT NULL ); --- Select all logs ordered by time desc +-- Select all logs ordered by time asc selectAll: -SELECT * FROM log ORDER BY time DESC; +SELECT * FROM log ORDER BY time ASC; -- Select logs filtered by message pattern selectFiltered: -SELECT * FROM log WHERE message LIKE ? ORDER BY time DESC; +SELECT * FROM log WHERE message LIKE ? ORDER BY time ASC; -- Insert a log entry insertLog: diff --git a/module/logging/logging/src/commonTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/FakeLogViewerComponent.kt b/module/logging/logging/src/commonTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/FakeLogViewerComponent.kt index 14a386b5..04f29ef1 100644 --- a/module/logging/logging/src/commonTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/FakeLogViewerComponent.kt +++ b/module/logging/logging/src/commonTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/FakeLogViewerComponent.kt @@ -8,7 +8,7 @@ import ru.bartwell.kick.module.logging.core.persist.LogEntity internal class FakeLogViewerComponent( initial: List, ) : LogViewerComponent { - private val allLogs = initial.sortedByDescending { it.time } + private val allLogs = initial.sortedBy { it.time } private val regex = Regex("\\[(.*?)]") private val _model = MutableValue( LogViewerState( @@ -30,6 +30,10 @@ internal class FakeLogViewerComponent( override fun onBackPressed() = Unit + override fun onAutoScrollToggleClick() { + _model.value = model.value.copy(isAutoScrollEnabled = !model.value.isAutoScrollEnabled) + } + override fun onClearAllClick() { _model.value = model.value.copy(log = emptyList()) } @@ -52,7 +56,6 @@ internal class FakeLogViewerComponent( } override fun onFilterApply() { - val q = model.value.filterQuery _model.value = model.value.copy( isFilterActive = true, isFilterDialogVisible = false, diff --git a/module/logging/logging/src/jvmTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerUiTest.kt b/module/logging/logging/src/jvmTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerUiTest.kt index 746973e9..a9f966b6 100644 --- a/module/logging/logging/src/jvmTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerUiTest.kt +++ b/module/logging/logging/src/jvmTest/kotlin/ru/bartwell/kick/module/logging/feature/table/presentation/LogViewerUiTest.kt @@ -27,7 +27,7 @@ class LogViewerUiTest { val composeTestRule = createComposeRule() @Test - fun new_messages_are_on_top() { + fun new_messages_are_at_bottom() { val logs = listOf( LogEntity(id = 1, time = 1_000L, level = LogLevel.INFO, message = "old"), LogEntity(id = 2, time = 3_000L, level = LogLevel.ERROR, message = "new"), @@ -39,14 +39,27 @@ class LogViewerUiTest { LogViewerContent(component = fake) } - // Expect sorted by time DESC: new, mid, old + // Expect sorted by time ASC: old, mid, new composeTestRule.onAllNodesWithTag("log_list").assertCountEquals(1) val items = composeTestRule.onAllNodesWithTag("log_item") items.assertCountEquals(3) items[0].assertIsDisplayed() - items[0].assert(hasTextContains("new")) + items[0].assert(hasTextContains("old")) items[1].assert(hasTextContains("mid")) - items[2].assert(hasTextContains("old")) + items[2].assert(hasTextContains("new")) + } + + @Test + fun auto_scroll_toggle_is_enabled_by_default() { + val logs = listOf( + LogEntity(id = 1, time = 1_000L, level = LogLevel.INFO, message = "m1"), + ) + val fake = FakeLogViewerComponent(logs) + + composeTestRule.setContent { LogViewerContent(component = fake) } + + composeTestRule.onNodeWithContentDescription("Disable auto-scroll").performClick() + composeTestRule.onNodeWithContentDescription("Enable auto-scroll").assertIsDisplayed() } @Test diff --git a/module/settings/control-panel-stub/src/commonMain/kotlin/ru/bartwell/kick/module/controlpanel/data/ControlPanelAccessor.kt b/module/settings/control-panel-stub/src/commonMain/kotlin/ru/bartwell/kick/module/controlpanel/data/ControlPanelAccessor.kt index 050cfcb1..f0c323b2 100644 --- a/module/settings/control-panel-stub/src/commonMain/kotlin/ru/bartwell/kick/module/controlpanel/data/ControlPanelAccessor.kt +++ b/module/settings/control-panel-stub/src/commonMain/kotlin/ru/bartwell/kick/module/controlpanel/data/ControlPanelAccessor.kt @@ -10,19 +10,37 @@ public class ControlPanelAccessor internal constructor() { public fun getBoolean(key: String): Boolean = ControlPanelSettings.get(key).value public fun getBooleanOrNull(key: String): Boolean? = ControlPanelSettings.getOrNull(key)?.value + public fun setBoolean(key: String, value: Boolean) { + ControlPanelSettings.put(key, InputType.Boolean(value)) + } public fun getInt(key: String): Int = ControlPanelSettings.get(key).value public fun getIntOrNull(key: String): Int? = ControlPanelSettings.getOrNull(key)?.value + public fun setInt(key: String, value: Int) { + ControlPanelSettings.put(key, InputType.Int(value)) + } public fun getLong(key: String): Long = ControlPanelSettings.get(key).value public fun getLongOrNull(key: String): Long? = ControlPanelSettings.getOrNull(key)?.value + public fun setLong(key: String, value: Long) { + ControlPanelSettings.put(key, InputType.Long(value)) + } public fun getFloat(key: String): Float = ControlPanelSettings.get(key).value public fun getFloatOrNull(key: String): Float? = ControlPanelSettings.getOrNull(key)?.value + public fun setFloat(key: String, value: Float) { + ControlPanelSettings.put(key, InputType.Float(value)) + } public fun getDouble(key: String): Double = ControlPanelSettings.get(key).value public fun getDoubleOrNull(key: String): Double? = ControlPanelSettings.getOrNull(key)?.value + public fun setDouble(key: String, value: Double) { + ControlPanelSettings.put(key, InputType.Double(value)) + } public fun getString(key: String): String = ControlPanelSettings.get(key).value public fun getStringOrNull(key: String): String? = ControlPanelSettings.getOrNull(key)?.value + public fun setString(key: String, value: String) { + ControlPanelSettings.put(key, InputType.String(value)) + } } diff --git a/module/settings/control-panel/src/commonMain/kotlin/ru/bartwell/kick/module/controlpanel/data/ControlPanelAccessor.kt b/module/settings/control-panel/src/commonMain/kotlin/ru/bartwell/kick/module/controlpanel/data/ControlPanelAccessor.kt index 18211870..9ef43ae4 100644 --- a/module/settings/control-panel/src/commonMain/kotlin/ru/bartwell/kick/module/controlpanel/data/ControlPanelAccessor.kt +++ b/module/settings/control-panel/src/commonMain/kotlin/ru/bartwell/kick/module/controlpanel/data/ControlPanelAccessor.kt @@ -10,19 +10,37 @@ public class ControlPanelAccessor internal constructor() { public fun getBoolean(key: String): Boolean = ControlPanelSettings.get(key).value public fun getBooleanOrNull(key: String): Boolean? = ControlPanelSettings.getOrNull(key)?.value + public fun setBoolean(key: String, value: Boolean) { + ControlPanelSettings.put(key, InputType.Boolean(value)) + } public fun getInt(key: String): Int = ControlPanelSettings.get(key).value public fun getIntOrNull(key: String): Int? = ControlPanelSettings.getOrNull(key)?.value + public fun setInt(key: String, value: Int) { + ControlPanelSettings.put(key, InputType.Int(value)) + } public fun getLong(key: String): Long = ControlPanelSettings.get(key).value public fun getLongOrNull(key: String): Long? = ControlPanelSettings.getOrNull(key)?.value + public fun setLong(key: String, value: Long) { + ControlPanelSettings.put(key, InputType.Long(value)) + } public fun getFloat(key: String): Float = ControlPanelSettings.get(key).value public fun getFloatOrNull(key: String): Float? = ControlPanelSettings.getOrNull(key)?.value + public fun setFloat(key: String, value: Float) { + ControlPanelSettings.put(key, InputType.Float(value)) + } public fun getDouble(key: String): Double = ControlPanelSettings.get(key).value public fun getDoubleOrNull(key: String): Double? = ControlPanelSettings.getOrNull(key)?.value + public fun setDouble(key: String, value: Double) { + ControlPanelSettings.put(key, InputType.Double(value)) + } public fun getString(key: String): String = ControlPanelSettings.get(key).value public fun getStringOrNull(key: String): String? = ControlPanelSettings.getOrNull(key)?.value + public fun setString(key: String, value: String) { + ControlPanelSettings.put(key, InputType.String(value)) + } }