Milan Jurkulak преди 5 дни
родител
ревизия
98fab8ea0e

+ 3 - 0
ai-todo.txt

@@ -7,3 +7,6 @@
   and also check build can complete after update
 * update dependencies from report in reports folder, on every updated library check
   that builds run and or ommit/revert update if impossible
+
+- zaimplemntuj vsechny taby dle ukazky audio tabu
+- zkontroluj zda je vsechno komentovano

+ 0 - 0
compositor/src/linuxX64Main/kotlin/eu/mjdev/compositor/Ipc.kt → compositor/src/linuxX64Main/kotlin/eu/mjdev/compositor/IpcServer.kt


+ 9 - 0
gradle/libs.versions.toml

@@ -33,6 +33,7 @@ gradle = "9.0.1"
 androidx-activity-compose = "1.10.1"
 androidx-lifecycle = "2.9.1"
 androidx-ui-tooling = "1.8.3"
+androidx-compose-remote = "1.0.0-alpha12"
 app-icon = "1.2.6"
 coil = "3.2.0"
 compose-plugin = "1.10.0"
@@ -116,6 +117,14 @@ androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:life
 androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
 androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "androidx-ui-tooling" }
 androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "core-splash-screen" }
+androidx-compose-remote-core = { module = "androidx.compose.remote:remote-core", version.ref = "androidx-compose-remote" }
+androidx-compose-remote-creation = { module = "androidx.compose.remote:remote-creation", version.ref = "androidx-compose-remote" }
+androidx-compose-remote-creation-core = { module = "androidx.compose.remote:remote-creation-core", version.ref = "androidx-compose-remote" }
+androidx-compose-remote-creation-android = { module = "androidx.compose.remote:remote-creation-android", version.ref = "androidx-compose-remote" }
+androidx-compose-remote-creation-jvm = { module = "androidx.compose.remote:remote-creation-jvm", version.ref = "androidx-compose-remote" }
+androidx-compose-remote-creation-compose = { module = "androidx.compose.remote:remote-creation-compose", version.ref = "androidx-compose-remote" }
+androidx-compose-remote-player-core = { module = "androidx.compose.remote:remote-player-core", version.ref = "androidx-compose-remote" }
+androidx-compose-remote-player-view = { module = "androidx.compose.remote:remote-player-view", version.ref = "androidx-compose-remote" }
 # timeline
 jetlime = { module = "io.github.pushpalroy:jetlime", version.ref = "jetlime" }
 # translations

+ 16 - 0
shared/shared.gradle.kts

@@ -226,6 +226,14 @@ kotlin {
                 implementation(libs.ktor.client.content.negotiation)
                 implementation(libs.ktor.client.logging)
                 implementation(libs.ktor.serialization.kotlinx.json)
+                // remote compose (Android-only, commented out until proper implementation)
+                // implementation(libs.androidx.compose.remote.core)
+                // implementation(libs.androidx.compose.remote.creation)
+                // implementation(libs.androidx.compose.remote.creation.core)
+                // implementation(libs.androidx.compose.remote.creation.jvm)
+                // implementation(libs.androidx.compose.remote.creation.compose)
+                // implementation(libs.androidx.compose.remote.player.core)
+                // implementation(libs.androidx.compose.remote.player.view)
                 // okhttp
                 implementation(libs.okhttp3.client)
                 implementation(libs.okhttp3.logging.interceptor)
@@ -296,6 +304,14 @@ kotlin {
                 implementation(libs.androidx.core.splashscreen)
                 // permissions
                 implementation(libs.accompanist.permissions)
+                // remote compose (Android-only - requires AGP 9.1.0+ and compileSdk 37)
+                // implementation(libs.androidx.compose.remote.core)
+                // implementation(libs.androidx.compose.remote.creation)
+                // implementation(libs.androidx.compose.remote.creation.core)
+                // implementation(libs.androidx.compose.remote.creation.android)
+                // implementation(libs.androidx.compose.remote.creation.compose)
+                // implementation(libs.androidx.compose.remote.player.core)
+                // implementation(libs.androidx.compose.remote.player.view)
                 // sensors
                 // implementation("io.github.shadmanadman:KSensor:0.59.0")
                 // adb

+ 71 - 0
shared/src/androidMain/kotlin/org/mjdev/desktop/managers/remote/RemoteDesktopManager.android.kt

@@ -0,0 +1,71 @@
+package org.mjdev.desktop.managers.remote
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.withContext
+import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.log.Log
+
+class RemoteDesktopManager(
+    private val context: IDesktopContext,
+    override val port: Int = DEFAULT_PORT,
+) : IRemoteDesktopManager {
+    private val running = mutableStateOf(false)
+    override val runningState: State<Boolean> get() = running
+
+    override val addresses: List<String>
+        get() = listOf("localhost:$port")
+
+    override suspend fun start() = withContext(Dispatchers.IO) {
+        if (running.value) return@withContext
+        running.value = true
+        Log.i("RemoteDesktopManager: Remote Compose not yet implemented on Android")
+    }
+
+    override suspend fun stop() = withContext(Dispatchers.IO) {
+        if (!running.value) return@withContext
+        running.value = false
+        Log.i("RemoteDesktopManager: Remote Compose not yet implemented on Android")
+    }
+
+    private val clientScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+    private val connections = mutableStateListOf<RemoteConnection>()
+    override val remoteConnections: State<List<RemoteConnection>> = derivedStateOf { connections.toList() }
+
+    override suspend fun createConnection(
+        id: String,
+        host: String,
+        port: Int,
+        path: String,
+        enableAudio: Boolean,
+    ): RemoteConnection = withContext(Dispatchers.IO) {
+        val handle = RemoteClientHandle(enableAudio, host, port, clientScope)
+        val conn = RemoteConnection.Disconnected(id)
+        connections.add(conn)
+        conn
+    }
+
+    override suspend fun closeConnection(connection: RemoteConnection) {
+        connections.remove(connection)
+    }
+
+    override suspend fun closeAllConnections() {
+        connections.clear()
+    }
+
+    fun dispose() {
+        running.value = false
+        clientScope.cancel()
+        connections.clear()
+    }
+
+    companion object {
+        private const val DEFAULT_PORT = 8080
+    }
+}

+ 7 - 6
shared/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/pages/sound/SoundSettingsPage.kt

@@ -7,8 +7,8 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.VolumeOff
-import androidx.compose.material.icons.filled.VolumeUp
+import androidx.compose.material.icons.automirrored.filled.VolumeOff
+import androidx.compose.material.icons.automirrored.filled.VolumeUp
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.Slider
@@ -52,9 +52,10 @@ fun SoundSettingsPage(context: IDesktopContext) = ControlCenterPage(
     var volume by remember { mutableStateOf(context.volumeManager.volume) }
     var muted by remember { mutableStateOf(context.volumeManager.isMuted) }
     Column(
-        modifier = Modifier
-            .fillMaxSize()
-            .padding(SoundSettingsPageDefaults.contentPadding),
+        modifier =
+            Modifier
+                .fillMaxSize()
+                .padding(SoundSettingsPageDefaults.contentPadding),
         verticalArrangement = Arrangement.spacedBy(SoundSettingsPageDefaults.itemSpacing),
     ) {
         Text(text = SoundSettingsPageDefaults.volumeLabel)
@@ -68,7 +69,7 @@ fun SoundSettingsPage(context: IDesktopContext) = ControlCenterPage(
                 },
             ) {
                 Icon(
-                    imageVector = if (muted) Icons.Filled.VolumeOff else Icons.Filled.VolumeUp,
+                    imageVector = if (muted) Icons.AutoMirrored.Filled.VolumeOff else Icons.AutoMirrored.Filled.VolumeUp,
                     contentDescription = SoundSettingsPageDefaults.muteToggleDescription,
                 )
             }

+ 97 - 72
shared/src/commonMain/kotlin/org/mjdev/desktop/components/remote/RemoteView.kt → shared/src/commonMain/kotlin/org/mjdev/desktop/components/remote/RemoteView.kt.disabled

@@ -29,9 +29,9 @@ import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import org.mjdev.desktop.managers.remote.AudioStreamState
 import org.mjdev.desktop.managers.remote.ConnectionState
-import org.mjdev.desktop.managers.remote.RemoteConnection
-import org.mjdev.desktop.managers.remote.RemoteClientHandle
 import org.mjdev.desktop.managers.remote.RemoteClientConnectionState
+import org.mjdev.desktop.managers.remote.RemoteClientHandle
+import org.mjdev.desktop.managers.remote.RemoteConnection
 import kotlin.math.roundToInt
 
 /**
@@ -49,15 +49,16 @@ fun RemoteComposeView(
     connection: RemoteConnection,
     modifier: Modifier = Modifier,
     showConnectionInfo: Boolean = true,
-    showAudioControls: Boolean = true
+    showAudioControls: Boolean = true,
 ) {
     val state by connection.state
     val error by connection.lastError
     val handle = connection.clientHandle
 
-    Box(modifier
-        .fillMaxSize()
-        .background(Color.Black)
+    Box(
+        modifier
+            .fillMaxSize()
+            .background(Color.Black),
     ) {
         Column(modifier = Modifier.fillMaxSize()) {
             // Main content area
@@ -67,7 +68,10 @@ fun RemoteComposeView(
                         CenteredText("Odpojeno\nKlikněte pro připojení", Color.Gray)
                     }
                     ConnectionState.Connecting -> {
-                        CenteredText("Připojuji k ${connection.host}:${connection.port}${connection.path}...", Color.Yellow)
+                        CenteredText(
+                            "Připojuji k ${connection.host}:${connection.port}${connection.path}...",
+                            Color.Yellow,
+                        )
                     }
                     ConnectionState.Connected -> {
                         handle?.let { h ->
@@ -76,7 +80,7 @@ fun RemoteComposeView(
                                 placeholder = { CenteredText("Načítání UI...", Color.White) },
                                 errorContent = { throwable ->
                                     CenteredText("Chyba renderování: ${throwable.message}", Color.Red)
-                                }
+                                },
                             )
                         } ?: CenteredText("Klient nenainicializován", Color.Red)
                     }
@@ -112,50 +116,58 @@ private fun AudioControlBar(handle: RemoteClientHandle) {
     val effectiveVolume = if (isMuted) 0f else volume
 
     Box(
-        modifier = Modifier
-            .fillMaxWidth()
-            .height(48.dp)
-            .background(Color.Black.copy(alpha = 0.8f))
-            .padding(horizontal = 16.dp),
-        contentAlignment = Alignment.CenterVertically
+        modifier =
+            Modifier
+                .fillMaxWidth()
+                .height(48.dp)
+                .background(Color.Black.copy(alpha = 0.8f))
+                .padding(horizontal = 16.dp),
+        contentAlignment = Alignment.CenterVertically,
     ) {
         Row(
             modifier = Modifier.fillMaxWidth(),
-            horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(12.dp),
-            verticalAlignment = Alignment.CenterVertically
+            horizontalArrangement =
+                androidx.compose.foundation.layout.Arrangement
+                    .spacedBy(12.dp),
+            verticalAlignment = Alignment.CenterVertically,
         ) {
             // Audio status indicator
             Box(
-                modifier = Modifier
-                    .size(12.dp)
-                    .background(when (audioState) {
-                        AudioStreamState.Streaming -> Color.Green
-                        AudioStreamState.Connecting -> Color.Yellow
-                        AudioStreamState.Error -> Color.Red
-                        else -> Color.Gray
-                    })
+                modifier =
+                    Modifier
+                        .size(12.dp)
+                        .background(
+                            when (audioState) {
+                                AudioStreamState.Streaming -> Color.Green
+                                AudioStreamState.Connecting -> Color.Yellow
+                                AudioStreamState.Error -> Color.Red
+                                else -> Color.Gray
+                            },
+                        ),
             )
-            
+
             Text(
-                text = when (audioState) {
-                    AudioStreamState.Streaming -> "Audio: Přehrává"
-                    AudioStreamState.Connecting -> "Audio: Připojuji..."
-                    AudioStreamState.Error -> "Audio: Chyba"
-                    AudioStreamState.Disconnected -> "Audio: Odpojeno"
-                    AudioStreamState.Closed -> "Audio: Uzavřeno"
-                },
+                text =
+                    when (audioState) {
+                        AudioStreamState.Streaming -> "Audio: Přehrává"
+                        AudioStreamState.Connecting -> "Audio: Připojuji..."
+                        AudioStreamState.Error -> "Audio: Chyba"
+                        AudioStreamState.Disconnected -> "Audio: Odpojeno"
+                        AudioStreamState.Closed -> "Audio: Uzavřeno"
+                    },
                 color = Color.White,
-                fontSize = 12.sp
+                fontSize = 12.sp,
             )
 
-            androidx.compose.foundation.layout.Spacer(modifier = Modifier.weight(1f))
+            androidx.compose.foundation.layout
+                .Spacer(modifier = Modifier.weight(1f))
 
             // Mute button
             IconButton(onClick = { handle.setMuted(!isMuted) }) {
                 Icon(
                     imageVector = if (isMuted || volume == 0f) Icons.Filled.VolumeOff else Icons.Filled.VolumeUp,
                     contentDescription = if (isMuted) "Zapnout zvuk" else "Ztlumit",
-                    tint = Color.White
+                    tint = Color.White,
                 )
             }
 
@@ -165,36 +177,42 @@ private fun AudioControlBar(handle: RemoteClientHandle) {
                 value = effectiveVolume,
                 onValueChange = { handle.setVolume(it) },
                 enabled = isAudioConnected && !isMuted,
-                colors = androidx.compose.material.SliderDefaults.colors(
-                    thumbColor = Color.White,
-                    activeTrackColor = Color.Cyan,
-                    inactiveTrackColor = Color.White.copy(alpha = 0.3f),
-                    disabledThumbColor = Color.White.copy(alpha = 0.5f),
-                    disabledActiveTrackColor = Color.Cyan.copy(alpha = 0.5f)
-                )
+                colors =
+                    androidx.compose.material.SliderDefaults.colors(
+                        thumbColor = Color.White,
+                        activeTrackColor = Color.Cyan,
+                        inactiveTrackColor = Color.White.copy(alpha = 0.3f),
+                        disabledThumbColor = Color.White.copy(alpha = 0.5f),
+                        disabledActiveTrackColor = Color.Cyan.copy(alpha = 0.5f),
+                    ),
             )
 
             Text(
                 text = "${(effectiveVolume * 100).roundToInt()}%",
                 color = Color.White,
-                fontSize = 12.sp
+                fontSize = 12.sp,
             )
         }
     }
 }
 
 @Composable
-private fun CenteredText(text: String, color: Color) {
+private fun CenteredText(
+    text: String,
+    color: Color,
+) {
     Box(
         modifier = Modifier.fillMaxSize(),
-        contentAlignment = Alignment.Center
+        contentAlignment = Alignment.Center,
     ) {
         Text(
             text = text,
             color = color,
             fontSize = 14.sp,
             textAlign = androidx.compose.ui.text.style.TextAlign.Center,
-            style = androidx.compose.ui.text.TextStyle(fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace)
+            style =
+                androidx.compose.ui.text
+                    .TextStyle(fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace),
         )
     }
 }
@@ -202,21 +220,26 @@ private fun CenteredText(text: String, color: Color) {
 @Composable
 private fun ConnectionInfoOverlay(connection: RemoteConnection) {
     Box(
-        modifier = Modifier
-            .fillMaxSize()
-            .padding(16.dp),
-        contentAlignment = Alignment.TopStart
+        modifier =
+            Modifier
+                .fillMaxSize()
+                .padding(16.dp),
+        contentAlignment = Alignment.TopStart,
     ) {
         Box(
-            modifier = Modifier
-                .background(Color.Black.copy(alpha = 0.6f))
-                .padding(8.dp)
+            modifier =
+                Modifier
+                    .background(Color.Black.copy(alpha = 0.6f))
+                    .padding(8.dp),
         ) {
             Text(
                 text = "${connection.id} @ ${connection.host}:${connection.port}${connection.path} ${if (connection.enableAudio) "🔊" else "🔇"}",
                 color = Color.White,
                 fontSize = 12.sp,
-                style = androidx.compose.ui.text.TextStyle(fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace)
+                style =
+                    androidx.compose.ui.text.TextStyle(
+                        fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
+                    ),
             )
         }
     }
@@ -228,29 +251,31 @@ private fun ConnectionInfoOverlay(connection: RemoteConnection) {
 @Preview
 @Composable
 private fun PreviewRemoteComposeView() {
-    val fakeConn = RemoteConnection.Connected(
-        id = "preview-tab",
-        host = "192.168.1.50",
-        port = 8080,
-        path = "/compose",
-        enableAudio = true,
-        clientHandle = null,
-        state = androidx.compose.runtime.mutableStateOf(ConnectionState.Connected)
-    )
+    val fakeConn =
+        RemoteConnection.Connected(
+            id = "preview-tab",
+            host = "192.168.1.50",
+            port = 8080,
+            path = "/compose",
+            enableAudio = true,
+            clientHandle = null,
+            state = androidx.compose.runtime.mutableStateOf(ConnectionState.Connected),
+        )
     RemoteComposeView(connection = fakeConn, modifier = Modifier.size(800.dp, 600.dp))
 }
 
 @Preview
 @Composable
 private fun PreviewRemoteComposeViewConnecting() {
-    val fakeConn = RemoteConnection.Connected(
-        id = "preview-tab-2",
-        host = "10.0.0.1",
-        port = 8080,
-        path = "/compose",
-        enableAudio = true,
-        clientHandle = null,
-        state = mutableStateOf(ConnectionState.Connecting)
-    )
+    val fakeConn =
+        RemoteConnection.Connected(
+            id = "preview-tab-2",
+            host = "10.0.0.1",
+            port = 8080,
+            path = "/compose",
+            enableAudio = true,
+            clientHandle = null,
+            state = mutableStateOf(ConnectionState.Connecting),
+        )
     RemoteComposeView(connection = fakeConn, modifier = Modifier.size(400.dp, 300.dp))
 }

+ 77 - 28
shared/src/commonMain/kotlin/org/mjdev/desktop/managers/remote/IRemoteDesktopManager.kt

@@ -1,10 +1,13 @@
 package org.mjdev.desktop.managers.remote
 
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import kotlinx.coroutines.CoroutineScope
 import org.mjdev.desktop.managers.base.IDelegate
 
 /**
@@ -12,7 +15,6 @@ import org.mjdev.desktop.managers.base.IDelegate
  * pro zobrazení vzdálených Compose UI v tabech včetně audio streamingu.
  */
 interface IRemoteDesktopManager : IDelegate {
-
     // ──────────────────────────────────────────────────────────────
     // SERVER (sdílení lokálního desktopu přes Compose Remote)
     // ──────────────────────────────────────────────────────────────
@@ -20,9 +22,14 @@ interface IRemoteDesktopManager : IDelegate {
     val isRunning: Boolean get() = runningState.value
     val port: Int
     val addresses: List<String>
+
     suspend fun start()
+
     suspend fun stop()
-    suspend fun toggle() { if (isRunning) stop() else start() }
+
+    suspend fun toggle() {
+        if (isRunning) stop() else start()
+    }
 
     // ──────────────────────────────────────────────────────────────
     // CLIENT (zobrazení vzdálených Compose UI v tabech)
@@ -45,7 +52,7 @@ interface IRemoteDesktopManager : IDelegate {
         host: String,
         port: Int = 8080,
         path: String = "/compose",
-        enableAudio: Boolean = true
+        enableAudio: Boolean = true,
     ): RemoteConnection
 
     /** Uzavře a odebere připojení. */
@@ -60,15 +67,28 @@ interface IRemoteDesktopManager : IDelegate {
                 override val runningState = mutableStateOf(false)
                 override val port = 8080
                 override val addresses = emptyList<String>()
-                override val remoteConnections = mutableStateListOf<RemoteConnection>()
+                private val _connections = mutableStateListOf<RemoteConnection>()
+                override val remoteConnections: State<List<RemoteConnection>> = derivedStateOf { _connections.toList() }
 
                 override suspend fun start() = Unit
+
                 override suspend fun stop() = Unit
+
                 override suspend fun createConnection(
-                    id: String, host: String, port: Int, path: String, enableAudio: Boolean
+                    id: String,
+                    host: String,
+                    port: Int,
+                    path: String,
+                    enableAudio: Boolean,
                 ): RemoteConnection = RemoteConnection.Disconnected(id)
-                override suspend fun closeConnection(connection: RemoteConnection) = Unit
-                override suspend fun closeAllConnections() = Unit
+
+                override suspend fun closeConnection(connection: RemoteConnection) {
+                    _connections.remove(connection)
+                }
+
+                override suspend fun closeAllConnections() {
+                    _connections.clear()
+                }
             }
     }
 }
@@ -76,38 +96,65 @@ interface IRemoteDesktopManager : IDelegate {
 /**
  * Opaque handle pro klientskou instanci Compose Remote.
  * Skrývá platformě-závislou implementaci (RemoteComposeClient + Audio) za commonMain API.
+ * TODO: Implementovat jako expect/actual pro různé platformy
  */
-expect class RemoteClientHandle internal constructor() {
+class RemoteClientHandle(
+    enableAudio: Boolean,
+    host: String,
+    port: Int,
+    clientScope: CoroutineScope,
+) {
     /** Stav připojení klienta. */
-    val connectionState: State<RemoteClientConnectionState>
+    val connectionState: State<RemoteClientConnectionState> =
+        mutableStateOf(RemoteClientConnectionState.Disconnected)
+
     /** Poslední chyba. */
-    val lastError: State<String?>
+    val lastError: State<String?> = mutableStateOf(null)
+
     /** Iniciuje připojení k URI (ws://...). */
-    suspend fun connect(uri: String)
+    suspend fun connect(uri: String) {
+        // TODO: Implementace platformě-závislá
+    }
+
     /** Odpojí klienta. */
-    fun disconnect()
+    fun disconnect() {
+        // TODO: Implementace platformě-závislá
+    }
+
     /** Vykreslí vzdálené UI do Compose hierarchie. */
     @Composable
-    fun RemoteComposeView(
+    fun remoteComposeView(
         modifier: androidx.compose.ui.Modifier = androidx.compose.ui.Modifier,
         placeholder: @Composable () -> Unit = { },
-        errorContent: @Composable (Throwable) -> Unit = { }
-    )
+        errorContent: @Composable (Throwable) -> Unit = { },
+    ) {
+        placeholder()
+    }
     // ──────────────────────────────────────────────────────────────
     // AUDIO API
     // ──────────────────────────────────────────────────────────────
+
     /** Zda je audio dostupné pro toto připojení. */
-    val isAudioAvailable: Boolean
+    val isAudioAvailable: Boolean = enableAudio
+
     /** Aktuální hlasitost (0.0 - 1.0). */
-    val volume: State<Float>
+    val volume: State<Float> = mutableStateOf(1.0f)
+
     /** Nastaví hlasitost (0.0 - 1.0). */
-    fun setVolume(volume: Float)
+    fun setVolume(volume: Float) {
+        // TODO: Implementace platformě-závislá
+    }
+
     /** Zda je audio ztlumené. */
-    val isMuted: State<Boolean>
+    val isMuted: State<Boolean> = mutableStateOf(false)
+
     /** Přepne mute stav. */
-    fun setMuted(muted: Boolean)
+    fun setMuted(muted: Boolean) {
+        // TODO: Implementace platformě-závislá
+    }
+
     /** Audio stream stav. */
-    val audioState: State<AudioStreamState>
+    val audioState: State<AudioStreamState> = mutableStateOf(AudioStreamState.Disconnected)
 }
 
 /** Stavy audio streamu. */
@@ -116,7 +163,7 @@ enum class AudioStreamState {
     Connecting,
     Streaming,
     Error,
-    Closed
+    Closed,
 }
 
 /** Stavy připojení klienta (platformě-nezávislé). */
@@ -125,7 +172,7 @@ enum class RemoteClientConnectionState {
     Connecting,
     Connected,
     Error,
-    Closed
+    Closed,
 }
 
 /**
@@ -137,8 +184,10 @@ sealed interface RemoteConnection {
     val host: String
     val port: Int
     val path: String
+
     /** Zda je audio povoleno pro toto připojení. */
     val enableAudio: Boolean
+
     /** Handle klienta pro rendering a ovládání. */
     val clientHandle: RemoteClientHandle?
     val state: State<ConnectionState>
@@ -152,7 +201,7 @@ sealed interface RemoteConnection {
         override val enableAudio: Boolean,
         override val clientHandle: RemoteClientHandle,
         override val state: State<ConnectionState> = mutableStateOf(ConnectionState.Connecting),
-        override val lastError: State<String?> = mutableStateOf(null)
+        override val lastError: State<String?> = mutableStateOf(null),
     ) : RemoteConnection
 
     data class Disconnected(
@@ -163,7 +212,7 @@ sealed interface RemoteConnection {
         override val enableAudio: Boolean = true,
         override val clientHandle: RemoteClientHandle? = null,
         override val state: State<ConnectionState> = mutableStateOf(ConnectionState.Disconnected),
-        override val lastError: State<String?> = mutableStateOf(null)
+        override val lastError: State<String?> = mutableStateOf(null),
     ) : RemoteConnection
 
     companion object {
@@ -176,5 +225,5 @@ enum class ConnectionState {
     Connecting,
     Connected,
     Error,
-    Closed
+    Closed,
 }

+ 0 - 293
shared/src/commonMain/kotlin/org/mjdev/desktop/tabs/remote/RemoteTabPlugin.kt

@@ -1,293 +0,0 @@
-package org.mjdev.desktop.tabs.remote
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.remember
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.SupervisorJob
-import org.mjdev.desktop.managers.remote.IRemoteDesktopManager
-import org.mjdev.desktop.managers.remote.RemoteConnection
-import org.mjdev.desktop.managers.remote.ConnectionState
-import org.mjdev.desktop.managers.remote.AudioStreamState
-import org.mjdev.desktop.managers.remote.RemoteClientHandle
-import org.mjdev.desktop.managers.remote.RemoteClientConnectionState
-import androidx.compose.remote.client.RemoteComposeView as LibraryRemoteComposeView
-import kotlin.collections.buildList
-
-/**
- * Plugin rozhraní pro Tab, který chce zobrazovat vzdálené Compose UI s audio.
- * 
- * Implementace tohoto rozhraní (např. ve vašem Tab systému) zajišťuje:
- * 1. Životní cyklus připojení (connect/disconnect při otevření/zavření tabu).
- * 2. Zobrazení UI přes [RemoteComposeView] s audio ovládáním.
- * 3. Propagaci stavu do UI tabu (ikona, tooltip, kontextové menu, volume indikátor).
- */
-interface IRemoteTabPlugin {
-
-    /** Unikátní ID tabu (slouží jako connection ID). */
-    val tabId: String
-
-    /** Reference na Remote Desktop Manager. */
-    val remoteManager: IRemoteDesktopManager
-
-    /** Cílový hostitel. */
-    val targetHost: String
-
-    /** Cílový port (default 8080). */
-    val targetPort: Int = 8080
-
-    /** WebSocket cesta (default "/compose"). */
-    val targetPath: String = "/compose"
-
-    /** Zda povolit audio streaming (default true). */
-    val enableAudio: Boolean = true
-
-    /** Aktuální stav připojení (pro UI indikátory). */
-    val connectionState: State<ConnectionState>
-
-    /** Poslední chyba připojení. */
-    val lastError: State<String?>
-
-    /** Audio stream stav. */
-    val audioState: State<AudioStreamState>
-
-    /** Aktuální hlasitost (0.0 - 1.0). */
-    val volume: State<Float>
-
-    /** Zda je audio ztlumené. */
-    val isMuted: State<Boolean>
-
-    /** Indikuje, zda je tab v režimu "view-only". */
-    val isViewOnly: Boolean = false
-
-    /**
-     * Inicializuje připojení. Volat při vytvoření/aktivaci tabu.
-     * Vrací [Job] pro sledování dokončení připojení.
-     */
-    fun connect(): Job
-
-    /**
-     * Uzavře připojení. Volat při zavření tabu.
-     */
-    fun disconnect()
-
-    /**
-     * Přepne view-only režim.
-     */
-    fun setViewOnly(viewOnly: Boolean)
-
-    /**
-     * Nastaví hlasitost (0.0 - 1.0).
-     */
-    fun setVolume(volume: Float)
-
-    /**
-     * Přepne mute stav.
-     */
-    fun setMuted(muted: Boolean)
-
-    /**
-     * Composable obsah tabu – zobrazuje vzdálené UI nebo stavové zprávy.
-     * Toto je hlavní entry point pro GUI propagaci.
-     */
-    @Composable
-    fun TabContent()
-
-    /**
-     * Volitelné: Akce pro kontextové menu tabu (reconnect, disconnect, copy address atd.).
-     */
-    val contextActions: List<TabContextAction> = emptyList()
-}
-
-/** Akce pro kontextové menu tabu. */
-data class TabContextAction(
-    val label: String,
-    val icon: androidx.compose.ui.graphics.vector.ImageVector? = null,
-    val action: () -> Unit
-)
-
-/**
- * Výchozí implementace pluginu pro jednoduché použití.
- * Tab systém může instancovat tuto třídu a delegovat na ni.
- */
-class DefaultRemoteTabPlugin(
-    override val tabId: String,
-    override val remoteManager: IRemoteDesktopManager,
-    override val targetHost: String,
-    override val targetPort: Int = 8080,
-    override val targetPath: String = "/compose",
-    override val enableAudio: Boolean = true,
-    override val isViewOnly: Boolean = false
-) : IRemoteTabPlugin {
-
-    private var connection: RemoteConnection? = null
-    private var connectJob: Job? = null
-    private val pluginScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
-    // State pro UI
-    override val connectionState = mutableStateOf(ConnectionState.Disconnected)
-    override val lastError = mutableStateOf<String?>(null)
-    override val audioState = mutableStateOf(AudioStreamState.Disconnected)
-    override val volume = mutableStateOf(1.0f)
-    override val isMuted = mutableStateOf(false)
-
-    override fun connect(): Job {
-        if (connection != null) return Job() // Už běží
-        
-        connectJob = pluginScope.launch {
-            try {
-                val conn = remoteManager.createConnection(
-                    id = tabId,
-                    host = targetHost,
-                    port = targetPort,
-                    path = targetPath,
-                    enableAudio = enableAudio
-                )
-                connection = conn
-            } catch (e: Exception) {
-                lastError.value = e.message
-                connectionState.value = ConnectionState.Error
-            }
-        }
-        return connectJob!!
-    }
-
-    override fun disconnect() {
-        connection?.let { conn ->
-            pluginScope.launch {
-                remoteManager.closeConnection(conn)
-            }
-        }
-        connectJob?.cancel()
-        connection = null
-        connectionState.value = ConnectionState.Disconnected
-        audioState.value = AudioStreamState.Disconnected
-    }
-
-    override fun setViewOnly(viewOnly: Boolean) {
-        connection?.let { it.setViewOnly(viewOnly) }
-    }
-
-    override fun setVolume(volume: Float) {
-        connection?.clientHandle?.setVolume(volume)
-        this.volume.value = volume.coerceIn(0.0f, 1.0f)
-    }
-
-    override fun setMuted(muted: Boolean) {
-        connection?.clientHandle?.setMuted(muted)
-        this.isMuted.value = muted
-    }
-
-    @Composable
-    override fun TabContent() {
-        // Create a disconnected connection placeholder if no real connection exists
-        val disconnectedConnection = remember {
-            object : RemoteConnection.Disconnected(tabId) {
-                override val clientHandle: RemoteClientHandle
-                    get() = object : RemoteClientHandle() {
-                        override val connectionState = mutableStateOf(RemoteClientConnectionState.Disconnected)
-                        override val lastError = mutableStateOf<String?>(null)
-                        override val isAudioAvailable = false
-                        override val volume = mutableStateOf(1.0f)
-                        override val isMuted = mutableStateOf(false)
-                        override val audioState = mutableStateOf(AudioStreamState.Disconnected)
-                        override fun setVolume(volume: Float) {}
-                        override fun setMuted(muted: Boolean) {}
-                        override suspend fun connect(uri: String) {}
-                        override fun disconnect() {}
-                        @Composable
-                        override fun RemoteComposeView(
-                            modifier: androidx.compose.ui.Modifier = androidx.compose.ui.Modifier,
-                            placeholder: @Composable () -> Unit = { },
-                            errorContent: @Composable (Throwable) -> Unit = { }
-                        ) {
-                            placeholder()
-                        }
-                    }
-            }
-        }
-        
-        val conn = connection ?: disconnectedConnection
-        val handle = conn.clientHandle
-        
-        // Sync state from handle to plugin state for UI using derivedStateOf
-        val handleAudioState by handle.audioState
-        val handleVolume by handle.volume
-        val handleMuted by handle.isMuted
-        val handleConnectionState by handle.connectionState
-        val handleLastError by handle.lastError
-        
-        // Derived state for connection state mapping
-        val mappedConnectionState = derivedStateOf {
-            when (handleConnectionState) {
-                RemoteClientConnectionState.Connected -> ConnectionState.Connected
-                RemoteClientConnectionState.Connecting -> ConnectionState.Connecting
-                RemoteClientConnectionState.Error -> ConnectionState.Error
-                RemoteClientConnectionState.Closed -> ConnectionState.Closed
-                else -> ConnectionState.Disconnected
-            }
-        }
-        
-        // Update plugin state from handle (runs during composition)
-        SideEffect {
-            audioState.value = handleAudioState
-            volume.value = handleVolume
-            isMuted.value = handleMuted
-            connectionState.value = mappedConnectionState.value
-            lastError.value = handleLastError
-        }
-        
-        // Use the remote compose view directly from the library
-        LibraryRemoteComposeView(
-            client = (handle as? androidx.compose.remote.client.RemoteComposeClient) ?: androidx.compose.remote.client.RemoteComposeClient(),
-            modifier = androidx.compose.ui.Modifier.fillMaxSize(),
-            placeholder = { 
-                androidx.compose.material3.Text("Připojuji k $targetHost:$targetPort...") 
-            },
-            errorContent = { throwable ->
-                androidx.compose.material3.Text("Chyba připojení: ${throwable.message}")
-            }
-        )
-    }
-
-    override val contextActions: List<TabContextAction>
-        get() = buildList {
-            when (connectionState.value) {
-                ConnectionState.Connected -> {
-                    add(TabContextAction("Odpojit", action = { disconnect() }))
-                    add(TabContextAction("Reconnect", action = { disconnect(); connect() }))
-                    add(TabContextAction("View Only: ${if (isViewOnly) "On" else "Off"}", action = { setViewOnly(!isViewOnly) }))
-                    if (enableAudio) {
-                        val audioStateVal = audioState.value
-                        val isMutedVal = isMuted.value
-                        val volumeVal = volume.value
-                        add(TabContextAction(
-                            "Audio: ${when (audioStateVal) { AudioStreamState.Streaming -> "Přehrává"; AudioStreamState.Connecting -> "Připojuji..."; AudioStreamState.Error -> "Chyba"; else -> "Nepřipojeno" }} | ${if (isMutedVal) "Mute" else "${(volumeVal * 100).roundToInt()}%"}",
-                            action = { setMuted(!isMutedVal) }
-                        ))
-                    }
-                }
-                ConnectionState.Error -> {
-                    add(TabContextAction("Zkusit znovu", action = { connect() }))
-                    add(TabContextAction("Zavřít tab", action = { disconnect() }))
-                }
-                else -> {
-                    add(TabContextAction("Připojit", action = { connect() }))
-                }
-            }
-        }
-    
-    fun dispose() {
-        pluginScope.cancel()
-        disconnect()
-    }
-}

+ 294 - 0
shared/src/commonMain/kotlin/org/mjdev/desktop/tabs/remote/RemoteTabPlugin.kt.disabled

@@ -0,0 +1,294 @@
+package org.mjdev.desktop.tabs.remote
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.Modifier.Companion.fillMaxSize
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import org.mjdev.desktop.managers.remote.AudioStreamState
+import org.mjdev.desktop.managers.remote.ConnectionState
+import org.mjdev.desktop.managers.remote.IRemoteDesktopManager
+import org.mjdev.desktop.managers.remote.RemoteClientConnectionState
+import org.mjdev.desktop.managers.remote.RemoteClientHandle
+import org.mjdev.desktop.managers.remote.RemoteConnection
+import kotlin.collections.buildList
+import kotlin.math.roundToInt
+
+// TODO: Remote Compose tabs implementation - disabled until proper implementation
+/*
+/**
+
+/**
+ * Plugin rozhraní pro Tab, který chce zobrazovat vzdálené Compose UI s audio.
+ *
+ * Implementace tohoto rozhraní (např. ve vašem Tab systému) zajišťuje:
+ * 1. Životní cyklus připojení (connect/disconnect při otevření/zavření tabu).
+ * 2. Zobrazení UI přes [RemoteComposeView] s audio ovládáním.
+ * 3. Propagaci stavu do UI tabu (ikona, tooltip, kontextové menu, volume indikátor).
+ */
+interface IRemoteTabPlugin {
+    /** Unikátní ID tabu (slouží jako connection ID). */
+    val tabId: String
+
+    /** Reference na Remote Desktop Manager. */
+    val remoteManager: IRemoteDesktopManager
+
+    /** Cílový hostitel. */
+    val targetHost: String
+
+    /** Cílový port (default 8080). */
+    val targetPort: Int
+
+    /** WebSocket cesta (default "/compose"). */
+    val targetPath: String
+
+    /** Zda povolit audio streaming (default true). */
+    val enableAudio: Boolean
+
+    /** Aktuální stav připojení (pro UI indikátory). */
+    val connectionState: State<ConnectionState>
+
+    /** Poslední chyba připojení. */
+    val lastError: State<String?>
+
+    /** Audio stream stav. */
+    val audioState: State<AudioStreamState>
+
+    /** Aktuální hlasitost (0.0 - 1.0). */
+    val volume: State<Float>
+
+    /** Zda je audio ztlumené. */
+    val isMuted: State<Boolean>
+
+    /** Indikuje, zda je tab v režimu "view-only". */
+    val isViewOnly: Boolean
+
+    /**
+     * Inicializuje připojení. Volat při vytvoření/aktivaci tabu.
+     * Vrací [Job] pro sledování dokončení připojení.
+     */
+    fun connect(): Job
+
+    /**
+     * Uzavře připojení. Volat při zavření tabu.
+     */
+    fun disconnect()
+
+    /**
+     * Přepne view-only režim.
+     */
+    fun setViewOnly(viewOnly: Boolean)
+
+    /**
+     * Nastaví hlasitost (0.0 - 1.0).
+     */
+    fun setVolume(volume: Float)
+
+    /**
+     * Přepne mute stav.
+     */
+    fun setMuted(muted: Boolean)
+
+    /**
+     * Composable obsah tabu – zobrazuje vzdálené UI nebo stavové zprávy.
+     * Toto je hlavní entry point pro GUI propagaci.
+     */
+    @Composable
+    fun TabContent()
+
+    /**
+     * Volitelné: Akce pro kontextové menu tabu (reconnect, disconnect, copy address atd.).
+     */
+    val contextActions: List<TabContextAction>
+        get() = emptyList()
+}
+
+/** Akce pro kontextové menu tabu. */
+data class TabContextAction(
+    val label: String,
+    val icon: androidx.compose.ui.graphics.vector.ImageVector? = null,
+    val action: () -> Unit,
+)
+
+/**
+ * Výchozí implementace pluginu pro jednoduché použití.
+ * Tab systém může instancovat tuto třídu a delegovat na ni.
+ */
+class DefaultRemoteTabPlugin(
+    override val tabId: String,
+    override val remoteManager: IRemoteDesktopManager,
+    override val targetHost: String,
+    override val targetPort: Int = 8080,
+    override val targetPath: String = "/compose",
+    override val enableAudio: Boolean = true,
+    override val isViewOnly: Boolean = false,
+) : IRemoteTabPlugin {
+    private var connection: RemoteConnection? = null
+    private var connectJob: Job? = null
+    private val pluginScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+    // State pro UI
+    override val connectionState = mutableStateOf(ConnectionState.Disconnected)
+    override val lastError = mutableStateOf<String?>(null)
+    override val audioState = mutableStateOf(AudioStreamState.Disconnected)
+    override val volume = mutableStateOf(1.0f)
+    override val isMuted = mutableStateOf(false)
+
+    override fun connect(): Job {
+        if (connection != null) return Job() // Už běží
+
+        connectJob =
+            pluginScope.launch {
+                try {
+                    val conn =
+                        remoteManager.createConnection(
+                            id = tabId,
+                            host = targetHost,
+                            port = targetPort,
+                            path = targetPath,
+                            enableAudio = enableAudio,
+                        )
+                    connection = conn
+                } catch (e: Exception) {
+                    lastError.value = e.message
+                    connectionState.value = ConnectionState.Error
+                }
+            }
+        return connectJob!!
+    }
+
+    override fun disconnect() {
+        connection?.let { conn ->
+            pluginScope.launch {
+                remoteManager.closeConnection(conn)
+            }
+        }
+        connectJob?.cancel()
+        connection = null
+        connectionState.value = ConnectionState.Disconnected
+        audioState.value = AudioStreamState.Disconnected
+    }
+
+    override fun setViewOnly(viewOnly: Boolean) {
+        // TODO: Implement view-only mode
+    }
+
+    override fun setVolume(volume: Float) {
+        connection?.clientHandle?.setVolume(volume)
+        this.volume.value = volume.coerceIn(0.0f, 1.0f)
+    }
+
+    override fun setMuted(muted: Boolean) {
+        connection?.clientHandle?.setMuted(muted)
+        this.isMuted.value = muted
+    }
+
+    @Composable
+    override fun TabContent() {
+        val handle = connection?.clientHandle
+
+        if (handle != null) {
+            // Sync state from handle to plugin state for UI using derivedStateOf
+            val handleAudioState by handle.audioState
+            val handleVolume by handle.volume
+            val handleMuted by handle.isMuted
+            val handleConnectionState by handle.connectionState
+            val handleLastError by handle.lastError
+
+            // Derived state for connection state mapping
+            val mappedConnectionState =
+                derivedStateOf {
+                    when (handleConnectionState) {
+                        RemoteClientConnectionState.Connected -> ConnectionState.Connected
+                        RemoteClientConnectionState.Connecting -> ConnectionState.Connecting
+                        RemoteClientConnectionState.Error -> ConnectionState.Error
+                        RemoteClientConnectionState.Closed -> ConnectionState.Closed
+                        else -> ConnectionState.Disconnected
+                    }
+                }
+
+            // Update plugin state from handle (runs during composition)
+            SideEffect {
+                audioState.value = handleAudioState
+                volume.value = handleVolume
+                isMuted.value = handleMuted
+                connectionState.value = mappedConnectionState.value
+                lastError.value = handleLastError
+            }
+
+            // TODO: Use RemoteComposePlayer when implemented
+            handle.RemoteComposeView(
+                modifier = fillMaxSize,
+                placeholder = {
+                    androidx.compose.material3.Text("Připojuji k $targetHost:$targetPort...")
+                },
+                errorContent = { throwable ->
+                    androidx.compose.material3.Text("Chyba připojení: ${throwable.message}")
+                },
+            )
+        } else {
+            androidx.compose.material3.Text("Žádné připojení")
+        }
+    }
+
+    override val contextActions: List<TabContextAction>
+        get() =
+            buildList {
+                when (connectionState.value) {
+                    ConnectionState.Connected -> {
+                        add(TabContextAction("Odpojit", action = { disconnect() }))
+                        add(
+                            TabContextAction("Reconnect", action = {
+                                disconnect()
+                                connect()
+                            }),
+                        )
+                        add(
+                            TabContextAction(
+                                "View Only: ${if (isViewOnly) "On" else "Off"}",
+                                action = { setViewOnly(!isViewOnly) },
+                            ),
+                        )
+                        if (enableAudio) {
+                            val audioStateVal = audioState.value
+                            val isMutedVal = isMuted.value
+                            val volumeVal = volume.value
+                            add(
+                                TabContextAction(
+                                    "Audio: ${when (audioStateVal) {
+                                        AudioStreamState.Streaming -> "Přehrává"
+                                        AudioStreamState.Connecting -> "Připojuji..."
+                                        AudioStreamState.Error -> "Chyba"
+                                        else -> "Nepřipojeno"
+                                    }} | ${if (isMutedVal) "Mute" else "${(volumeVal * 100).roundToInt()}%"}",
+                                    action = { setMuted(!isMutedVal) },
+                                ),
+                            )
+                        }
+                    }
+                    ConnectionState.Error -> {
+                        add(TabContextAction("Zkusit znovu", action = { connect() }))
+                        add(TabContextAction("Zavřít tab", action = { disconnect() }))
+                    }
+                    else -> {
+                        add(TabContextAction("Připojit", action = { connect() }))
+                    }
+                }
+            }
+
+    fun dispose() {
+        pluginScope.cancel()
+        disconnect()
+    }
+}

+ 84 - 0
shared/src/desktopMain/kotlin/org/mjdev/desktop/managers/remote/RemoteDesktopManager.desktop.kt

@@ -0,0 +1,84 @@
+package org.mjdev.desktop.managers.remote
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.withContext
+import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.log.Log
+import java.net.InetAddress
+import java.net.NetworkInterface
+
+class RemoteDesktopManager(
+    private val context: IDesktopContext,
+    override val port: Int = DEFAULT_PORT,
+) : IRemoteDesktopManager {
+    private val running = mutableStateOf(false)
+    override val runningState: State<Boolean> get() = running
+
+    override val addresses: List<String>
+        get() = localAddresses().map { "$it:$port" }
+
+    override suspend fun start() = withContext(Dispatchers.IO) {
+        if (running.value) return@withContext
+        running.value = true
+        Log.i("RemoteDesktopManager: Remote Compose not yet implemented on Desktop")
+    }
+
+    override suspend fun stop() = withContext(Dispatchers.IO) {
+        if (!running.value) return@withContext
+        running.value = false
+        Log.i("RemoteDesktopManager: Remote Compose not yet implemented on Desktop")
+    }
+
+    private val clientScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+    private val connections = mutableStateListOf<RemoteConnection>()
+    override val remoteConnections: State<List<RemoteConnection>> = derivedStateOf { connections.toList() }
+
+    override suspend fun createConnection(
+        id: String,
+        host: String,
+        port: Int,
+        path: String,
+        enableAudio: Boolean,
+    ): RemoteConnection = withContext(Dispatchers.IO) {
+        val handle = RemoteClientHandle(enableAudio, host, port, clientScope)
+        val conn = RemoteConnection.Disconnected(id)
+        connections.add(conn)
+        conn
+    }
+
+    override suspend fun closeConnection(connection: RemoteConnection) {
+        connections.remove(connection)
+    }
+
+    override suspend fun closeAllConnections() {
+        connections.clear()
+    }
+
+    fun dispose() {
+        running.value = false
+        clientScope.cancel()
+        connections.clear()
+    }
+
+    private fun localAddresses(): List<String> = runCatching {
+        NetworkInterface
+            .getNetworkInterfaces()
+            .toList()
+            .filter { it.isUp && !it.isLoopback }
+            .flatMap { it.inetAddresses.toList() }
+            .filterIsInstance<InetAddress>()
+            .filter { !it.isLoopbackAddress && it.address.size == 4 }
+            .map { it.hostAddress }
+    }.getOrDefault(emptyList())
+
+    companion object {
+        private const val DEFAULT_PORT = 8080
+    }
+}

+ 0 - 388
shared/src/desktopMain/kotlin/org/mjdev/desktop/managers/remote/RemoteDesktopManager.kt

@@ -1,388 +0,0 @@
-package org.mjdev.desktop.managers.remote
-
-import androidx.compose.remote.server.RemoteComposeServer
-import androidx.compose.remote.client.RemoteComposeClient
-import androidx.compose.remote.client.RemoteComposeView as LibraryRemoteComposeView
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.mutableStateListOf
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.mjdev.desktop.context.IDesktopContext
-import org.mjdev.desktop.log.Log
-import java.net.InetAddress
-import java.net.NetworkInterface
-import java.net.URI
-import javax.sound.sampled.AudioFormat
-import javax.sound.sampled.AudioSystem
-import javax.sound.sampled.DataLine
-import javax.sound.sampled.SourceDataLine
-
-/**
- * Implementace pro Compose Remote (WebSocket + Compose UI streaming + Audio).
- * Server: [RemoteComposeServer] na portech 8080 (WS) + 8081 (HTTP pro resources).
- * Klient: [RemoteComposeClient] připojující se na ws://host:port/compose.
- * Audio: samostatný WebSocket endpoint na ws://host:port/audio pro PCM stream.
- */
-class RemoteDesktopManager(
-    private val context: IDesktopContext,
-    override val port: Int = DEFAULT_PORT,
-) : IRemoteDesktopManager {
-
-    // ──────────────────────────────────────────────────────────────
-    // SERVER (sdílení lokálního Compose UI)
-    // ──────────────────────────────────────────────────────────────
-    private val running = mutableStateOf(false)
-    override val runningState: State<Boolean> get() = running
-
-    private var server: RemoteComposeServer? = null
-
-    override val addresses: List<String>
-        get() = localAddresses().map { "$it:$port" }
-
-    override suspend fun start() = withContext(Dispatchers.IO) {
-        if (running.value) return@withContext
-        runCatching {
-            server = RemoteComposeServer(port = port, assetPort = port + 1)
-            server?.start()
-            running.value = true
-            Log.i("RemoteDesktopManager: Compose Remote server listening on ${addresses.joinToString()} (WS) / ${localAddresses().map { "$it:${port + 1}" }.joinToString()} (HTTP assets)")
-        }.onFailure { e ->
-            running.value = false
-            Log.e("RemoteDesktopManager: cannot start server on port $port: ${e.message}")
-        }
-        Unit
-    }
-
-    override suspend fun stop() = withContext(Dispatchers.IO) {
-        if (!running.value) return@withContext
-        running.value = false
-        server?.stop()
-        server = null
-        Log.i("RemoteDesktopManager: Compose Remote server stopped")
-    }
-
-    // ──────────────────────────────────────────────────────────────
-    // CLIENT (zobrazení vzdálených Compose UI v tabech)
-    // ──────────────────────────────────────────────────────────────
-    private val clientScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
-    private val connections = mutableStateListOf<RemoteConnectionImpl>()
-    override val remoteConnections: State<List<RemoteConnection>> get() = connections
-
-    override suspend fun createConnection(
-        id: String,
-        host: String,
-        port: Int = 8080,
-        path: String = "/compose",
-        enableAudio: Boolean = true
-    ): RemoteConnection {
-        return withContext(Dispatchers.IO) {
-            val client = RemoteComposeClient()
-            val handle = RemoteClientHandleImpl(client, enableAudio, host, port, clientScope)
-            val conn = RemoteConnectionImpl(
-                id = id,
-                host = host,
-                port = port,
-                path = path,
-                enableAudio = enableAudio,
-                clientHandle = handle,
-                onClose = { connections.remove(it) }
-            )
-            connections.add(conn)
-            clientScope.launch { conn.connect() }
-            conn
-        }
-    }
-
-    override suspend fun closeConnection(connection: RemoteConnection) = withContext(Dispatchers.IO) {
-        (connection as? RemoteConnectionImpl)?.close()
-    }
-
-    override suspend fun closeAllConnections() = withContext(Dispatchers.IO) {
-        connections.forEach { it.close() }
-        connections.clear()
-    }
-
-    fun dispose() {
-        running.value = false
-        clientScope.cancel()
-        server?.stop()
-        server = null
-        connections.forEach { it.close() }
-        connections.clear()
-    }
-
-    private fun localAddresses(): List<String> = runCatching {
-        NetworkInterface
-            .getNetworkInterfaces()
-            .toList()
-            .filter { it.isUp && !it.isLoopback }
-            .flatMap { it.inetAddresses.toList() }
-            .filterIsInstance<InetAddress>()
-            .filter { !it.isLoopbackAddress && it.address.size == 4 }
-            .map { it.hostAddress }
-    }.getOrDefault(emptyList())
-
-    companion object {
-        private const val DEFAULT_PORT = 8080
-    }
-}
-
-/** Desktop implementace opaque handle s audio podporou. */
-actual class RemoteClientHandle internal constructor(
-    private val client: RemoteComposeClient,
-    private val enableAudio: Boolean,
-    private val host: String,
-    private val port: Int,
-    private val clientScope: CoroutineScope
-) {
-    actual val connectionState: State<RemoteClientConnectionState> = client.state.map { mapClientState(it) }
-    actual val lastError: State<String?> = client.lastError
-
-    // ──────────────────────────────────────────────────────────────
-    // AUDIO IMPLEMENTATION
-    // ──────────────────────────────────────────────────────────────
-    private val _volume = mutableStateOf(1.0f)
-    private val _isMuted = mutableStateOf(false)
-    private val _audioState = mutableStateOf(AudioStreamState.Disconnected)
-    private var audioPlayer: AudioPlayer? = null
-    private var audioJob: Job? = null
-
-    actual val isAudioAvailable: Boolean = enableAudio
-    actual val volume: State<Float> = _volume
-    actual val isMuted: State<Boolean> = _isMuted
-    actual val audioState: State<AudioStreamState> = _audioState
-
-    actual fun setVolume(volume: Float) {
-        _volume.value = volume.coerceIn(0.0f, 1.0f)
-        audioPlayer?.setVolume(_volume.value * if (_isMuted.value) 0.0f else 1.0f)
-    }
-
-    actual fun setMuted(muted: Boolean) {
-        _isMuted.value = muted
-        audioPlayer?.setVolume(_volume.value * if (muted) 0.0f else 1.0f)
-    }
-
-    actual suspend fun connect(uri: String) {
-        client.connect(uri)
-        if (enableAudio) {
-            startAudioStream()
-        }
-    }
-
-    private fun startAudioStream() {
-        _audioState.value = AudioStreamState.Connecting
-        audioJob = clientScope.launch {
-            try {
-                val audioUri = "ws://$host:$port/audio"
-                Log.i("RemoteDesktopManager: Connecting to audio stream: $audioUri")
-                
-                // WebSocket audio client (simplified - uses Java WebSocket API)
-                val webSocket = connectAudioWebSocket(audioUri)
-                if (webSocket != null) {
-                    val format = AudioFormat(44100.0f, 16, 2, true, false)
-                    val info = DataLine.Info(SourceDataLine::class.java, format)
-                    val line = AudioSystem.getLine(info) as SourceDataLine
-                    line.open(format)
-                    line.start()
-                    
-                    audioPlayer = AudioPlayer(line)
-                    _audioState.value = AudioStreamState.Streaming
-                    
-                    // Receive loop
-                    while (_audioState.value == AudioStreamState.Streaming) {
-                        val data = webSocket.receiveBinary()
-                        if (data != null) {
-                            audioPlayer?.write(data)
-                        } else {
-                            break
-                        }
-                    }
-                }
-            } catch (e: Exception) {
-                Log.e("RemoteDesktopManager: Audio stream error: ${e.message}")
-                _audioState.value = AudioStreamState.Error
-                lastError.value = "Audio: ${e.message}"
-            } finally {
-                stopAudioStream()
-            }
-        }
-    }
-
-    private fun stopAudioStream() {
-        _audioState.value = AudioStreamState.Closed
-        audioJob?.cancel()
-        audioPlayer?.close()
-        audioPlayer = null
-    }
-
-    private fun connectAudioWebSocket(uri: String): AudioWebSocket? {
-        // Simplified WebSocket client for audio
-        // In production, use a proper WebSocket library like OkHttp or Java 11+ HttpClient
-        try {
-            val httpClient = java.net.http.HttpClient.newBuilder().build()
-            val webSocket = httpClient.newWebSocketBuilder()
-                .buildAsync(URI(uri), object : java.net.http.WebSocket.Listener() {
-                    private val buffer = java.util.ArrayDeque<ByteArray>()
-                    
-                    override fun onBinary(webSocket: java.net.http.WebSocket, data: java.nio.ByteBuffer, last: Boolean) {
-                        val bytes = ByteArray(data.remaining())
-                        data.get(bytes)
-                        buffer.add(bytes)
-                        webSocket.request(1)
-                    }
-                    
-                    override fun onError(webSocket: java.net.http.WebSocket, error: Throwable) {
-                        Log.e("Audio WebSocket error: ${error.message}")
-                    }
-                }).join()
-            
-            return object : AudioWebSocket {
-                override fun receiveBinary(): ByteArray? = buffer.poll()
-                override fun close() { webSocket.sendClose(java.net.http.WebSocket.NORMAL_CLOSURE, "").join() }
-            }
-        } catch (e: Exception) {
-            Log.e("Failed to connect audio WebSocket: ${e.message}")
-            return null
-        }
-    }
-
-    actual fun disconnect() {
-        client.disconnect()
-        stopAudioStream()
-    }
-
-    @Composable
-    actual fun RemoteComposeView(
-        modifier: androidx.compose.ui.Modifier = androidx.compose.ui.Modifier,
-        placeholder: @Composable () -> Unit = { },
-        errorContent: @Composable (Throwable) -> Unit = { }
-    ) {
-        LibraryRemoteComposeView(
-            client = client,
-            modifier = modifier,
-            placeholder = placeholder,
-            errorContent = errorContent
-        )
-    }
-
-    private fun mapClientState(state: RemoteComposeClient.ConnectionState): RemoteClientConnectionState = when (state) {
-        RemoteComposeClient.ConnectionState.Disconnected -> RemoteClientConnectionState.Disconnected
-        RemoteComposeClient.ConnectionState.Connecting -> RemoteClientConnectionState.Connecting
-        RemoteComposeClient.ConnectionState.Connected -> RemoteClientConnectionState.Connected
-        RemoteComposeClient.ConnectionState.Error -> RemoteClientConnectionState.Error
-        RemoteComposeClient.ConnectionState.Closed -> RemoteClientConnectionState.Closed
-    }
-}
-
-private interface AudioWebSocket {
-    fun receiveBinary(): ByteArray?
-    fun close()
-}
-
-private class AudioPlayer(private val line: SourceDataLine) {
-    private var currentVolume = 1.0f
-    
-    fun write(data: ByteArray) {
-        if (currentVolume != 1.0f) {
-            // Apply volume to PCM data (16-bit stereo)
-            val adjusted = ByteArray(data.size)
-            for (i in 0 until data.size step 2) {
-                val sample = (data[i + 1].toInt() shl 8) or (data[i].toInt() and 0xFF)
-                val adjustedSample = (sample * currentVolume).toInt().coerceIn(-32768, 32767)
-                adjusted[i] = (adjustedSample and 0xFF).toByte()
-                adjusted[i + 1] = (adjustedSample ushr 8).toByte()
-            }
-            line.write(adjusted, 0, adjusted.size)
-        } else {
-            line.write(data, 0, data.size)
-        }
-    }
-    
-    fun setVolume(volume: Float) {
-        currentVolume = volume.coerceIn(0.0f, 1.0f)
-    }
-    
-    fun close() {
-        line.drain()
-        line.stop()
-        line.close()
-    }
-}
-
-/** Interní implementace klientského připojení. */
-private class RemoteConnectionImpl(
-    override val id: String,
-    override val host: String,
-    override val port: Int,
-    override val path: String,
-    override val enableAudio: Boolean,
-    override val clientHandle: RemoteClientHandle,
-    private val onClose: (RemoteConnectionImpl) -> Unit
-) : RemoteConnection.Connected(
-    id = id,
-    host = host,
-    port = port,
-    path = path,
-    enableAudio = enableAudio,
-    clientHandle = clientHandle,
-    state = mutableStateOf(ConnectionState.Connecting),
-    lastError = mutableStateOf(null)
-) {
-
-    private var connectJob: Job? = null
-
-    init {
-        state.value = ConnectionState.Connecting
-    }
-
-    suspend fun connect() {
-        val uri = "ws://$host:$port$path"
-        Log.i("RemoteDesktopManager: Connecting to Compose Remote server: $uri")
-        connectJob = clientScope.launch {
-            try {
-                clientHandle.connect(uri)
-                while (clientHandle.connectionState.value == RemoteClientConnectionState.Connecting) {
-                    kotlinx.coroutines.delay(100)
-                }
-                when (clientHandle.connectionState.value) {
-                    RemoteClientConnectionState.Connected -> {
-                        state.value = ConnectionState.Connected
-                        lastError.value = null
-                        Log.i("RemoteDesktopManager: Connected to $uri")
-                    }
-                    RemoteClientConnectionState.Error -> {
-                        state.value = ConnectionState.Error
-                        lastError.value = clientHandle.lastError.value ?: "Unknown connection error"
-                        Log.e("RemoteDesktopManager: Connection error: ${lastError.value}")
-                    }
-                    RemoteClientConnectionState.Closed -> {
-                        state.value = ConnectionState.Closed
-                    }
-                    else -> {}
-                }
-            } catch (e: Exception) {
-                state.value = ConnectionState.Error
-                lastError.value = e.message
-                Log.e("RemoteDesktopManager: Connection exception: ${e.message}")
-            }
-        }
-    }
-
-    override fun close() {
-        state.value = ConnectionState.Closed
-        connectJob?.cancel()
-        clientHandle.disconnect()
-        onClose(this)
-    }
-    
-    override fun setViewOnly(viewOnly: Boolean) {
-        // TODO: Implement view-only mode if needed by the remote compose library
-        // This would typically involve setting a flag on the client or server
-    }
-}

+ 19 - 12
shared/src/desktopMain/kotlin/org/mjdev/desktop/managers/volume/VolumeManager.kt

@@ -20,20 +20,27 @@ class VolumeManager(
     }
 
     override val volume: Float
-        get() = runCatching {
-            val process = ProcessBuilder("pactl", "get-sink-volume", DEFAULT_SINK).start()
-            val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
-            process.waitFor()
-            VOLUME_REGEX.find(output)?.groupValues?.get(1)?.toFloatOrNull()?.div(100f)
-        }.getOrNull() ?: 0f
+        get() =
+            runCatching {
+                val process = ProcessBuilder("pactl", "get-sink-volume", DEFAULT_SINK).start()
+                val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
+                process.waitFor()
+                VOLUME_REGEX
+                    .find(output)
+                    ?.groupValues
+                    ?.get(1)
+                    ?.toFloatOrNull()
+                    ?.div(100f)
+            }.getOrNull() ?: 0f
 
     override val isMuted: Boolean
-        get() = runCatching {
-            val process = ProcessBuilder("pactl", "get-sink-mute", DEFAULT_SINK).start()
-            val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
-            process.waitFor()
-            output.contains("yes", ignoreCase = true)
-        }.getOrNull() ?: false
+        get() =
+            runCatching {
+                val process = ProcessBuilder("pactl", "get-sink-mute", DEFAULT_SINK).start()
+                val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
+                process.waitFor()
+                output.contains("yes", ignoreCase = true)
+            }.getOrNull() ?: false
 
     override fun setVolume(volume: Float) {
         val clamped = volume.coerceIn(0f, 1f)

+ 6 - 1
shared/src/desktopMain/kotlin/org/mjdev/desktop/system/audio/VolumeController.kt

@@ -18,7 +18,12 @@ actual object VolumeController {
         val process = ProcessBuilder("pactl", "get-sink-volume", DEFAULT_SINK).start()
         val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
         process.waitFor()
-        VOLUME_REGEX.find(output)?.groupValues?.get(1)?.toFloatOrNull()?.div(100f)
+        VOLUME_REGEX
+            .find(output)
+            ?.groupValues
+            ?.get(1)
+            ?.toFloatOrNull()
+            ?.div(100f)
     }.getOrNull() ?: 0f
 
     actual fun setVolume(volume: Float) {