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
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ Kick.controlPanel.getString("list")
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"))`:

```
Kick.controlPanel.event.collect { event ->
Kick.controlPanel.events.collect { event ->
when (event) {
is ControlPanelEvent.ButtonClicked -> when (event.id) {
"refresh_cache" -> refreshCache()
Expand Down Expand Up @@ -417,6 +417,20 @@ Button("Kick") {
}
```

To close the viewer programmatically, call `Kick.close()`:

In Kotlin:

```kotlin
Kick.close()
```

In Swift:

```swift
KickKt.shared.close()
```

## Shortcuts

By default, Kick adds a shortcut to your app’s launcher icon (accessible via long-press). To disable it, pass `enableShortcut = false` during initialization:
Expand Down
5 changes: 5 additions & 0 deletions main-core/src/commonMain/kotlin/ru/bartwell/kick/Kick.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public interface Kick {
public val modules: List<Module>
public fun launch(context: PlatformContext)
public fun launch(context: PlatformContext, startScreen: StartScreen?)
public fun close()
public fun getShortcutId(): String

public companion object Companion {
Expand All @@ -34,6 +35,10 @@ public interface Kick {
instance?.launch(context, startScreen)
}

public fun close() {
instance?.close()
}

public fun getShortcutId(): String = instance?.getShortcutId() ?: ""
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,12 @@ internal class EmptyKickImpl : Kick {
)
}

override fun close() {
println(
"Kick: Unable to close the viewer because a stub module has been added. " +
"Please ensure that both the `main-core` and `main-runtime` modules are correctly configured"
)
}

override fun getShortcutId(): String = ""
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.arkivanov.decompose.defaultComponentContext
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
Expand All @@ -13,12 +17,20 @@ import ru.bartwell.kick.core.component.Config
import ru.bartwell.kick.core.data.StartScreen
import ru.bartwell.kick.core.util.WindowStateManager
import ru.bartwell.kick.runtime.core.component.DefaultRootComponent
import ru.bartwell.kick.runtime.core.util.ViewerCommands

public class KickActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
ViewerCommands.closeRequests.collect {
finish()
}
}
}
val modules = Kick.modules
val startScreenJson = intent.getStringExtra(EXTRA_START_SCREEN)
val startScreen = if (startScreenJson != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ru.bartwell.kick.core.data.Theme
import ru.bartwell.kick.core.util.WindowStateManager
import ru.bartwell.kick.runtime.core.util.LaunchManager
import ru.bartwell.kick.runtime.core.util.ShortcutManager
import ru.bartwell.kick.runtime.core.util.ViewerCommands
import ru.bartwell.kick.runtime.core.util.WindowStateManagerImpl
import ru.bartwell.kick.runtime.core.util.id

Expand All @@ -34,6 +35,10 @@ internal class KickImpl(
LaunchManager.launch(context, modules, startScreen)
}

override fun close() {
ViewerCommands.requestClose()
}

override fun getShortcutId(): String = ShortcutManager.id
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ru.bartwell.kick.runtime.core.util

import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow

internal object ViewerCommands {
private val _closeRequests = MutableSharedFlow<Unit>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)

val closeRequests: SharedFlow<Unit> = _closeRequests

fun requestClose() {
_closeRequests.tryEmit(Unit)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ru.bartwell.kick.runtime.core.util

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.window.ComposeUIViewController
import com.arkivanov.decompose.DefaultComponentContext
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
Expand Down Expand Up @@ -41,6 +42,11 @@ internal object IosSceneController {
startScreen = startScreen,
)
val uiViewController = ComposeUIViewController(configure = { enforceStrictPlistSanityCheck = false }) {
LaunchedEffect(Unit) {
ViewerCommands.closeRequests.collect {
dismiss()
}
}
App(rootComponent)
}
lifecycle.create()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ru.bartwell.kick.runtime.core.util

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.awt.ComposeWindow
import com.arkivanov.decompose.DefaultComponentContext
Expand Down Expand Up @@ -55,6 +56,12 @@ internal actual object LaunchManager {
})

setContent {
LaunchedEffect(Unit) {
ViewerCommands.closeRequests.collect {
window.dispose()
WindowStateManager.getInstance()?.setWindowClosed()
}
}
CompositionLocalProvider(LocalComposeWindow provides window) {
App(rootComponent)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ru.bartwell.kick.runtime.core.util

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import com.arkivanov.decompose.DefaultComponentContext
Expand Down Expand Up @@ -50,6 +51,13 @@ internal actual object LaunchManager {
)

ComposeViewport(root) {
LaunchedEffect(Unit) {
ViewerCommands.closeRequests.collect {
val el = document.getElementById(ROOT_ID)
el?.parentElement?.removeChild(el)
WindowStateManager.getInstance()?.setWindowClosed()
}
}
CompositionLocalProvider(LocalOverlayRoot provides root) {
App(rootComponent)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ru.bartwell.kick.module.controlpanel.persists.ControlPanelSettings

@Suppress("TooManyFunctions")
public class ControlPanelAccessor internal constructor() {
public val event: Flow<ControlPanelEvent> = ControlPanelActions.events
public val events: Flow<ControlPanelEvent> = ControlPanelActions.events

public fun getBoolean(key: String): Boolean = ControlPanelSettings.get<InputType.Boolean>(key).value
public fun getBooleanOrNull(key: String): Boolean? = ControlPanelSettings.getOrNull<InputType.Boolean>(key)?.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ru.bartwell.kick.module.controlpanel.core.persists.ControlPanelSettings

@Suppress("TooManyFunctions")
public class ControlPanelAccessor internal constructor() {
public val event: Flow<ControlPanelEvent> = ControlPanelActions.events
public val events: Flow<ControlPanelEvent> = ControlPanelActions.events

public fun getBoolean(key: String): Boolean = ControlPanelSettings.get<InputType.Boolean>(key).value
public fun getBooleanOrNull(key: String): Boolean? = ControlPanelSettings.getOrNull<InputType.Boolean>(key)?.value
Expand Down
47 changes: 43 additions & 4 deletions sample/ios/iosSample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import shared
enum DatabaseType: String, CaseIterable, Identifiable {
case sqlDelight = "SqlDelight"
case room = "Room"

var id: String { self.rawValue }
}

struct ContentView: View {
@State private var selectedTheme: AppTheme = .auto

@State private var showButtonAlert = false
@State private var isCollectingEvents = false
@State private var controlPanelCollector: ControlPanelEventCollector?

var body: some View {
VStack(spacing: 20) {
Picker("Select theme", selection: $selectedTheme) {
Expand All @@ -20,7 +23,7 @@ struct ContentView: View {
}
.pickerStyle(SegmentedPickerStyle())
.padding()

Button("Launch viewer") {
KickKt.shared.launch(context: PlatformContextKt.getPlatformContext())
}
Expand All @@ -34,9 +37,32 @@ struct ContentView: View {
.preferredColorScheme(colorScheme(for: selectedTheme))
.onAppear {
KickKt.shared.theme = selectedTheme.toLibraryTheme()
startControlPanelEventCollection()
}
.alert("You clicked the button", isPresented: $showButtonAlert) {
Button("OK", role: .cancel) { }
}
}


private func startControlPanelEventCollection() {
guard !isCollectingEvents else { return }
isCollectingEvents = true
let collector = ControlPanelEventCollector { event in
print("Control panel event: \(event)")
if let clicked = event as? ControlPanelEvent.ButtonClicked,
clicked.id == "show_alert" {
KickKt.shared.close()
showButtonAlert = true
}
}
controlPanelCollector = collector
KickCompanion.shared.controlPanel.events.collect(collector: collector) { error in
if let error = error {
print("Control panel event collection error: \(error)")
}
}
}

private func colorScheme(for theme: AppTheme) -> ColorScheme? {
switch theme {
case .auto:
Expand All @@ -48,3 +74,16 @@ struct ContentView: View {
}
}
}

final class ControlPanelEventCollector: Kotlinx_coroutines_coreFlowCollector {
private let onEvent: (Any?) -> Void

init(onEvent: @escaping (Any?) -> Void) {
self.onEvent = onEvent
}

func emit(value: Any?, completionHandler: @escaping (Error?) -> Void) {
onEvent(value)
completionHandler(nil)
}
}
2 changes: 2 additions & 0 deletions sample/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ kotlin {
export(projects.mainCore)
if (isRelease) {
export(projects.mainRuntimeStub)
export(projects.controlPanelStub)
} else {
export(projects.mainRuntime)
export(projects.controlPanel)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.DropdownMenuItem
Expand All @@ -32,20 +33,26 @@ import ru.bartwell.kick.core.data.Theme
import ru.bartwell.kick.core.data.platformContext
import ru.bartwell.kick.core.ui.ExposedDropdownMenuBox
import ru.bartwell.kick.module.controlpanel.controlPanel
import ru.bartwell.kick.module.controlpanel.data.ControlPanelEvent

@Composable
fun App() {
val context = platformContext()
var selectedTheme by remember { mutableStateOf(AppTheme.Auto) }
var showButtonAlert by remember { mutableStateOf(false) }

LaunchedEffect(selectedTheme) {
Kick.theme = selectedTheme.toLibraryTheme()
println("Configuration test: featureEnabled=" + Kick.controlPanel.getBoolean("featureEnabled"))
}

LaunchedEffect(Unit) {
Kick.controlPanel.event.collect { event ->
Kick.controlPanel.events.collect { event ->
println("Control panel event: $event")
if (event is ControlPanelEvent.ButtonClicked && event.id == "show_alert") {
Kick.close()
showButtonAlert = true
}
}
}

Expand All @@ -67,6 +74,17 @@ fun App() {
)
}
}
if (showButtonAlert) {
AlertDialog(
onDismissRequest = { showButtonAlert = false },
confirmButton = {
Button(onClick = { showButtonAlert = false }) {
Text("OK")
}
},
text = { Text("You clicked the button") },
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,9 @@ class TestDataInitializer(context: PlatformContext) {
type = ActionType.Button("aaaaaa"),
),
ControlPanelItem(
name = "Log B",
name = "Show alert",
category = "Actions",
type = ActionType.Button("bbbbbb"),
type = ActionType.Button("show_alert"),
),
)
}
Expand Down