Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
12 changes: 11 additions & 1 deletion content/docs/Advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"))`:
Expand Down Expand Up @@ -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.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ru.bartwell.kick.module.logging.core.persist.LogEntity
internal class FakeLogViewerComponent(
initial: List<LogEntity>,
) : LogViewerComponent {
private val allLogs = initial.sortedByDescending { it.time }
private val allLogs = initial.sortedBy { it.time }
private val regex = Regex("\\[(.*?)]")
private val _model = MutableValue(
LogViewerState(
Expand All @@ -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())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public interface LogViewerComponent : Component {
public val model: Value<LogViewerState>

public fun onBackPressed()
public fun onAutoScrollToggleClick()
public fun onClearAllClick()
public fun onFilterClick()
public fun onFilterDialogDismiss()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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")
}
Expand 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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ru.bartwell.kick.module.logging.core.persist.LogEntity
public data class LogViewerState(
val log: List<LogEntity> = emptyList(),
val error: String? = null,
val isAutoScrollEnabled: Boolean = true,
val filterQuery: String = "",
val isFilterActive: Boolean = false,
val isFilterDialogVisible: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ru.bartwell.kick.module.logging.core.persist.LogEntity
internal class FakeLogViewerComponent(
initial: List<LogEntity>,
) : LogViewerComponent {
private val allLogs = initial.sortedByDescending { it.time }
private val allLogs = initial.sortedBy { it.time }
private val regex = Regex("\\[(.*?)]")
private val _model = MutableValue(
LogViewerState(
Expand All @@ -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())
}
Expand All @@ -52,7 +56,6 @@ internal class FakeLogViewerComponent(
}

override fun onFilterApply() {
val q = model.value.filterQuery
_model.value = model.value.copy(
isFilterActive = true,
isFilterDialogVisible = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,37 @@ public class ControlPanelAccessor internal constructor() {

public fun getBoolean(key: String): Boolean = ControlPanelSettings.get<InputType.Boolean>(key).value
public fun getBooleanOrNull(key: String): Boolean? = ControlPanelSettings.getOrNull<InputType.Boolean>(key)?.value
public fun setBoolean(key: String, value: Boolean) {
ControlPanelSettings.put(key, InputType.Boolean(value))
}

public fun getInt(key: String): Int = ControlPanelSettings.get<InputType.Int>(key).value
public fun getIntOrNull(key: String): Int? = ControlPanelSettings.getOrNull<InputType.Int>(key)?.value
public fun setInt(key: String, value: Int) {
ControlPanelSettings.put(key, InputType.Int(value))
}

public fun getLong(key: String): Long = ControlPanelSettings.get<InputType.Long>(key).value
public fun getLongOrNull(key: String): Long? = ControlPanelSettings.getOrNull<InputType.Long>(key)?.value
public fun setLong(key: String, value: Long) {
ControlPanelSettings.put(key, InputType.Long(value))
}

public fun getFloat(key: String): Float = ControlPanelSettings.get<InputType.Float>(key).value
public fun getFloatOrNull(key: String): Float? = ControlPanelSettings.getOrNull<InputType.Float>(key)?.value
public fun setFloat(key: String, value: Float) {
ControlPanelSettings.put(key, InputType.Float(value))
}

public fun getDouble(key: String): Double = ControlPanelSettings.get<InputType.Double>(key).value
public fun getDoubleOrNull(key: String): Double? = ControlPanelSettings.getOrNull<InputType.Double>(key)?.value
public fun setDouble(key: String, value: Double) {
ControlPanelSettings.put(key, InputType.Double(value))
}

public fun getString(key: String): String = ControlPanelSettings.get<InputType.String>(key).value
public fun getStringOrNull(key: String): String? = ControlPanelSettings.getOrNull<InputType.String>(key)?.value
public fun setString(key: String, value: String) {
ControlPanelSettings.put(key, InputType.String(value))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,37 @@ public class ControlPanelAccessor internal constructor() {

public fun getBoolean(key: String): Boolean = ControlPanelSettings.get<InputType.Boolean>(key).value
public fun getBooleanOrNull(key: String): Boolean? = ControlPanelSettings.getOrNull<InputType.Boolean>(key)?.value
public fun setBoolean(key: String, value: Boolean) {
ControlPanelSettings.put(key, InputType.Boolean(value))
}

public fun getInt(key: String): Int = ControlPanelSettings.get<InputType.Int>(key).value
public fun getIntOrNull(key: String): Int? = ControlPanelSettings.getOrNull<InputType.Int>(key)?.value
public fun setInt(key: String, value: Int) {
ControlPanelSettings.put(key, InputType.Int(value))
}

public fun getLong(key: String): Long = ControlPanelSettings.get<InputType.Long>(key).value
public fun getLongOrNull(key: String): Long? = ControlPanelSettings.getOrNull<InputType.Long>(key)?.value
public fun setLong(key: String, value: Long) {
ControlPanelSettings.put(key, InputType.Long(value))
}

public fun getFloat(key: String): Float = ControlPanelSettings.get<InputType.Float>(key).value
public fun getFloatOrNull(key: String): Float? = ControlPanelSettings.getOrNull<InputType.Float>(key)?.value
public fun setFloat(key: String, value: Float) {
ControlPanelSettings.put(key, InputType.Float(value))
}

public fun getDouble(key: String): Double = ControlPanelSettings.get<InputType.Double>(key).value
public fun getDoubleOrNull(key: String): Double? = ControlPanelSettings.getOrNull<InputType.Double>(key)?.value
public fun setDouble(key: String, value: Double) {
ControlPanelSettings.put(key, InputType.Double(value))
}

public fun getString(key: String): String = ControlPanelSettings.get<InputType.String>(key).value
public fun getStringOrNull(key: String): String? = ControlPanelSettings.getOrNull<InputType.String>(key)?.value
public fun setString(key: String, value: String) {
ControlPanelSettings.put(key, InputType.String(value))
}
}