diff --git a/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/Main.kt b/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/Main.kt index 96d9a3e..042a10d 100644 --- a/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/Main.kt +++ b/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/Main.kt @@ -5,6 +5,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -24,12 +26,12 @@ import dev.zacsweers.metro.createGraph import kotlinx.coroutines.launch import java.awt.Desktop import java.net.URI +import java.util.UUID fun main() { configurePlatformAppearance() - val appGraph = createGraph() application { - val windowState = rememberPersistedWindowState() + val windows = remember { mutableStateListOf(createInspectorWindow()) } val updateController = remember { UpdateController( checker = UpdateChecker(), @@ -47,21 +49,40 @@ fun main() { AutoCheckDecision.Disabled -> Unit } } - Window( - onCloseRequest = ::exitApplication, - title = "Snap-O Network Inspector", - state = windowState, - ) { - SnapOMenuBar( - controller = updateController, - onCheckForUpdates = { - scope.launch { - updateController.checkForUpdates(UpdateCheckSource.Manual) - } - }, - onCloseRequest = ::exitApplication, - ) - App(appGraph) + + fun openNewWindow() { + windows.add(createInspectorWindow()) + } + + fun closeWindow(window: InspectorWindow) { + window.graph.store.stop() + windows.remove(window) + if (windows.isEmpty()) { + exitApplication() + } + } + + windows.forEach { window -> + key(window.id) { + val windowState = rememberPersistedWindowState() + Window( + onCloseRequest = { closeWindow(window) }, + title = "Snap-O Network Inspector", + state = windowState, + ) { + SnapOMenuBar( + controller = updateController, + onNewWindow = ::openNewWindow, + onCheckForUpdates = { + scope.launch { + updateController.checkForUpdates(UpdateCheckSource.Manual) + } + }, + onCloseRequest = { closeWindow(window) }, + ) + App(window.graph) + } + } } } } @@ -93,3 +114,12 @@ private fun openSnapOUpdate() { } } } + +private data class InspectorWindow( + val id: String = UUID.randomUUID().toString(), + val graph: AppGraph, +) + +private fun createInspectorWindow(): InspectorWindow { + return InspectorWindow(graph = createGraph()) +} diff --git a/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/inspector/NetworkInspectorStore.kt b/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/inspector/NetworkInspectorStore.kt index bea68ec..17c329f 100644 --- a/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/inspector/NetworkInspectorStore.kt +++ b/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/inspector/NetworkInspectorStore.kt @@ -6,6 +6,7 @@ import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -73,6 +74,11 @@ class NetworkInspectorStore( } } + fun stop() { + service.stop() + scope.cancel() + } + fun notifyFeatureOpened(feature: String, serverId: SnapOLinkServerId?) { service.sendFeatureOpened(feature, serverId) } diff --git a/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/update/UpdateUi.kt b/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/update/UpdateUi.kt index a3f5790..8ae2609 100644 --- a/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/update/UpdateUi.kt +++ b/snapo-desktop-compose/src/main/kotlin/com/openai/snapo/desktop/update/UpdateUi.kt @@ -9,11 +9,17 @@ import androidx.compose.ui.window.MenuBar @Composable internal fun FrameWindowScope.SnapOMenuBar( controller: UpdateController, + onNewWindow: () -> Unit, onCheckForUpdates: () -> Unit, onCloseRequest: () -> Unit, ) { MenuBar { Menu("File") { + Item( + text = "New Window", + shortcut = KeyShortcut(Key.N, meta = true), + onClick = onNewWindow, + ) Item( text = "Close", shortcut = KeyShortcut(Key.W, meta = true),