Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5a779e48a | |||
| 4e868d9f7f | |||
| e4ca414721 | |||
| 48405ca2ee | |||
| 305fc48093 | |||
| 90e5d029ce | |||
| 4ec0bf8a85 | |||
| bf9bc51084 |
@@ -1,4 +1,3 @@
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
@@ -66,11 +65,11 @@ kotlin {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "moe.uni.comfy_kmp"
|
||||
namespace = "moe.uni.comfyKmp"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.uni.comfy_kmp"
|
||||
applicationId = "moe.uni.comfyKmp"
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import android.content.Context
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import android.os.Build
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import moe.uni.comfy_kmp.AndroidAppContext
|
||||
import moe.uni.comfyKmp.AndroidAppContext
|
||||
|
||||
actual fun saveCoverImage(bytes: ByteArray, filenameHint: String): String {
|
||||
val safeName = filenameHint.replace(Regex("[^A-Za-z0-9._-]"), "_")
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.network
|
||||
package moe.uni.comfyKmp.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
@@ -1,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">comfy_kmp</string>
|
||||
<string name="app_name">comfyKmp</string>
|
||||
</resources>
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -6,10 +6,10 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import moe.uni.comfy_kmp.di.AppContainer
|
||||
import moe.uni.comfy_kmp.di.LocalAppContainer
|
||||
import moe.uni.comfy_kmp.ui.screens.ServerScreen
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfyTheme
|
||||
import moe.uni.comfyKmp.di.AppContainer
|
||||
import moe.uni.comfyKmp.di.LocalAppContainer
|
||||
import moe.uni.comfyKmp.ui.screens.ServerScreen
|
||||
import moe.uni.comfyKmp.ui.theme.ComfyTheme
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
class Greeting {
|
||||
private val platform = getPlatform()
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
interface Platform {
|
||||
val name: String
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toPath
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import okio.FileSystem
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
expect suspend fun saveImageToGallery(bytes: ByteArray, filenameHint: String): String
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.di
|
||||
package moe.uni.comfyKmp.di
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
|
||||
+7
-7
@@ -1,11 +1,11 @@
|
||||
package moe.uni.comfy_kmp.di
|
||||
package moe.uni.comfyKmp.di
|
||||
|
||||
import moe.uni.comfy_kmp.network.ComfyApiClient
|
||||
import moe.uni.comfy_kmp.network.ComfyWebSocketClient
|
||||
import moe.uni.comfy_kmp.network.createHttpClient
|
||||
import moe.uni.comfy_kmp.storage.ServerRepository
|
||||
import moe.uni.comfy_kmp.storage.SettingsStorage
|
||||
import moe.uni.comfy_kmp.storage.WorkflowRepository
|
||||
import moe.uni.comfyKmp.network.ComfyApiClient
|
||||
import moe.uni.comfyKmp.network.ComfyWebSocketClient
|
||||
import moe.uni.comfyKmp.network.createHttpClient
|
||||
import moe.uni.comfyKmp.storage.ServerRepository
|
||||
import moe.uni.comfyKmp.storage.SettingsStorage
|
||||
import moe.uni.comfyKmp.storage.WorkflowRepository
|
||||
|
||||
class AppContainer(
|
||||
private val storage: SettingsStorage = SettingsStorage()
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.network
|
||||
package moe.uni.comfyKmp.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
@@ -10,8 +10,8 @@ import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import moe.uni.comfy_kmp.data.PromptRequest
|
||||
import moe.uni.comfy_kmp.data.PromptResponse
|
||||
import moe.uni.comfyKmp.data.PromptRequest
|
||||
import moe.uni.comfyKmp.data.PromptResponse
|
||||
|
||||
class ComfyApiClient(
|
||||
private val httpClient: HttpClient
|
||||
@@ -0,0 +1,197 @@
|
||||
package moe.uni.comfyKmp.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.websocket.webSocket
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.utils.io.errors.IOException
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import moe.uni.comfyKmp.data.ComfyJson
|
||||
import moe.uni.comfyKmp.data.WsMessage
|
||||
import kotlin.math.min
|
||||
import kotlin.random.Random
|
||||
|
||||
data class ReconnectPolicy(
|
||||
val maxAttempts: Int,
|
||||
val initialDelayMs: Long,
|
||||
val maxDelayMs: Long,
|
||||
val jitterMs: Long
|
||||
)
|
||||
|
||||
enum class WsConnectionStatus {
|
||||
IDLE,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
RECONNECTING,
|
||||
FAILED,
|
||||
DISCONNECTED
|
||||
}
|
||||
|
||||
data class WsConnectionState(
|
||||
val status: WsConnectionStatus,
|
||||
val attempt: Int = 0,
|
||||
val maxAttempts: Int = 0
|
||||
)
|
||||
|
||||
class ComfyWebSocketClient(
|
||||
private val httpClient: HttpClient
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val _events = MutableSharedFlow<WsMessage>(extraBufferCapacity = 64)
|
||||
val events: SharedFlow<WsMessage> = _events
|
||||
private val _connectionState = MutableStateFlow(WsConnectionState(WsConnectionStatus.IDLE))
|
||||
val connectionState: StateFlow<WsConnectionState> = _connectionState.asStateFlow()
|
||||
|
||||
private var session: WebSocketSession? = null
|
||||
private var connectJob: Job? = null
|
||||
private var shouldReconnect = false
|
||||
|
||||
fun connect(
|
||||
baseUrl: String,
|
||||
clientId: String,
|
||||
policy: ReconnectPolicy = DEFAULT_RECONNECT_POLICY
|
||||
) {
|
||||
if (connectJob?.isActive == true) return
|
||||
shouldReconnect = true
|
||||
val wsUrl = buildWsUrl(baseUrl, clientId)
|
||||
connectJob = scope.launch {
|
||||
val maxAttempts = policy.maxAttempts
|
||||
var retryCount = 0
|
||||
try {
|
||||
while (isActive && shouldReconnect) {
|
||||
val status = if (retryCount == 0) {
|
||||
WsConnectionStatus.CONNECTING
|
||||
} else {
|
||||
WsConnectionStatus.RECONNECTING
|
||||
}
|
||||
_connectionState.value = WsConnectionState(
|
||||
status = status,
|
||||
attempt = retryCount,
|
||||
maxAttempts = maxAttempts
|
||||
)
|
||||
try {
|
||||
httpClient.webSocket(wsUrl) {
|
||||
session = this
|
||||
_connectionState.value = WsConnectionState(WsConnectionStatus.CONNECTED)
|
||||
retryCount = 0
|
||||
try {
|
||||
for (frame in incoming) {
|
||||
if (frame is Frame.Text) {
|
||||
val element = ComfyJson.parseToJsonElement(frame.readText())
|
||||
val obj = element.jsonObject
|
||||
val type = obj["type"]?.jsonPrimitive?.content ?: "unknown"
|
||||
val data = obj["data"] as? JsonObject
|
||||
_events.tryEmit(WsMessage(type = type, data = data))
|
||||
}
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
// 后台/断网时底层 socket 关闭会抛异常,这里忽略即可
|
||||
} finally {
|
||||
session = null
|
||||
}
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (_: Exception) {
|
||||
session = null
|
||||
}
|
||||
|
||||
if (!shouldReconnect || !isActive) break
|
||||
retryCount += 1
|
||||
if (retryCount > maxAttempts) {
|
||||
_connectionState.value = WsConnectionState(
|
||||
status = WsConnectionStatus.FAILED,
|
||||
attempt = maxAttempts,
|
||||
maxAttempts = maxAttempts
|
||||
)
|
||||
shouldReconnect = false
|
||||
break
|
||||
}
|
||||
|
||||
_connectionState.value = WsConnectionState(
|
||||
status = WsConnectionStatus.RECONNECTING,
|
||||
attempt = retryCount,
|
||||
maxAttempts = maxAttempts
|
||||
)
|
||||
delay(calculateBackoffDelayMs(policy, retryCount))
|
||||
}
|
||||
} finally {
|
||||
if (!shouldReconnect && _connectionState.value.status != WsConnectionStatus.FAILED) {
|
||||
_connectionState.value = WsConnectionState(WsConnectionStatus.DISCONNECTED)
|
||||
}
|
||||
connectJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disconnect() {
|
||||
shouldReconnect = false
|
||||
connectJob?.cancelAndJoin()
|
||||
connectJob = null
|
||||
try {
|
||||
session?.close()
|
||||
} catch (_: IOException) {
|
||||
// Ignore close errors
|
||||
} finally {
|
||||
session = null
|
||||
}
|
||||
_connectionState.value = WsConnectionState(WsConnectionStatus.DISCONNECTED)
|
||||
}
|
||||
|
||||
fun disconnectAsync() {
|
||||
scope.launch {
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
shouldReconnect = false
|
||||
connectJob?.cancel()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun buildWsUrl(baseUrl: String, clientId: String): String {
|
||||
val trimmed = baseUrl.trimEnd('/')
|
||||
val scheme = if (trimmed.startsWith("https://")) "wss://" else "ws://"
|
||||
val host = trimmed.removePrefix("https://").removePrefix("http://")
|
||||
return "${scheme}${host}/ws?clientId=$clientId"
|
||||
}
|
||||
|
||||
private fun calculateBackoffDelayMs(policy: ReconnectPolicy, attempt: Int): Long {
|
||||
val exponent = (attempt - 1).coerceAtLeast(0).coerceAtMost(30)
|
||||
val baseDelay = policy.initialDelayMs * (1L shl exponent)
|
||||
val cappedDelay = min(policy.maxDelayMs, baseDelay)
|
||||
val jitter = if (policy.jitterMs > 0) {
|
||||
Random.nextLong(0, policy.jitterMs + 1)
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
return cappedDelay + jitter
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_RECONNECT_POLICY = ReconnectPolicy(
|
||||
maxAttempts = 5,
|
||||
initialDelayMs = 1_000,
|
||||
maxDelayMs = 30_000,
|
||||
jitterMs = 250
|
||||
)
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
package moe.uni.comfy_kmp.network
|
||||
package moe.uni.comfyKmp.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import moe.uni.comfy_kmp.data.ComfyJson
|
||||
import moe.uni.comfyKmp.data.ComfyJson
|
||||
|
||||
internal fun HttpClientConfig<*>.installComfyPlugins() {
|
||||
install(ContentNegotiation) {
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package moe.uni.comfy_kmp.storage
|
||||
package moe.uni.comfyKmp.storage
|
||||
|
||||
import moe.uni.comfy_kmp.data.ServerConfig
|
||||
import moe.uni.comfy_kmp.data.WorkflowEntity
|
||||
import moe.uni.comfyKmp.data.ServerConfig
|
||||
import moe.uni.comfyKmp.data.WorkflowEntity
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Clock
|
||||
|
||||
+5
-6
@@ -1,13 +1,12 @@
|
||||
package moe.uni.comfy_kmp.storage
|
||||
package moe.uni.comfyKmp.storage
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import moe.uni.comfy_kmp.data.ComfyJson
|
||||
import moe.uni.comfy_kmp.data.ServerConfig
|
||||
import moe.uni.comfy_kmp.data.WorkflowEntity
|
||||
import moe.uni.comfy_kmp.data.saveCoverImage
|
||||
import moe.uni.comfyKmp.data.ComfyJson
|
||||
import moe.uni.comfyKmp.data.ServerConfig
|
||||
import moe.uni.comfyKmp.data.WorkflowEntity
|
||||
import moe.uni.comfyKmp.data.saveCoverImage
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toPath
|
||||
import kotlin.io.encoding.Base64
|
||||
+3
-6
@@ -1,8 +1,6 @@
|
||||
package moe.uni.comfy_kmp.ui.components
|
||||
package moe.uni.comfyKmp.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -14,7 +12,6 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -25,8 +22,8 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfy_kmp.ui.theme.comfyColors
|
||||
import moe.uni.comfyKmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfyKmp.ui.theme.comfyColors
|
||||
|
||||
@Composable
|
||||
fun ComfyDialog(
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.ui.components
|
||||
package moe.uni.comfyKmp.ui.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
@@ -23,8 +23,8 @@ import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfy_kmp.ui.theme.comfyColors
|
||||
import moe.uni.comfyKmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfyKmp.ui.theme.comfyColors
|
||||
|
||||
@Composable
|
||||
fun GlassCard(
|
||||
+50
-49
@@ -1,20 +1,19 @@
|
||||
package moe.uni.comfy_kmp.ui.components
|
||||
package moe.uni.comfyKmp.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -23,9 +22,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ErrorOutline
|
||||
import androidx.compose.material.icons.filled.HourglassEmpty
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -38,8 +35,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfy_kmp.ui.theme.comfyColors
|
||||
import moe.uni.comfyKmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfyKmp.ui.theme.comfyColors
|
||||
|
||||
data class GalleryImage(
|
||||
val filename: String,
|
||||
@@ -52,26 +49,30 @@ fun ImageGallery(
|
||||
images: List<GalleryImage>,
|
||||
modifier: Modifier = Modifier,
|
||||
onImageClick: ((Int) -> Unit)? = null,
|
||||
onSaveClick: ((Int) -> Unit)? = null,
|
||||
onSetCoverClick: ((Int) -> Unit)? = null
|
||||
onSaveClick: ((Int) -> Unit)? = null
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
|
||||
val availableWidth = maxWidth
|
||||
val availableHeight = maxHeight
|
||||
|
||||
if (images.isEmpty()) {
|
||||
EmptyGalleryState(modifier = Modifier.align(Alignment.Center))
|
||||
} else {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(ComfySpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(ComfySpacing.md),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
itemsIndexed(images) { index, image ->
|
||||
GalleryImageItem(
|
||||
image = image,
|
||||
availableWidth = availableWidth,
|
||||
availableHeight = availableHeight,
|
||||
onClick = {
|
||||
onImageClick?.invoke(index)
|
||||
},
|
||||
onSave = onSaveClick?.let { { it(index) } },
|
||||
onSetCover = onSetCoverClick?.let { { it(index) } }
|
||||
onSave = onSaveClick?.let { { it(index) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -82,38 +83,57 @@ fun ImageGallery(
|
||||
@Composable
|
||||
private fun GalleryImageItem(
|
||||
image: GalleryImage,
|
||||
availableWidth: androidx.compose.ui.unit.Dp,
|
||||
availableHeight: androidx.compose.ui.unit.Dp,
|
||||
onClick: () -> Unit,
|
||||
onSave: (() -> Unit)?,
|
||||
onSetCover: (() -> Unit)?
|
||||
onSave: (() -> Unit)?
|
||||
) {
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
val aspectRatio = image.bitmap?.let { it.width.toFloat() / it.height.toFloat() } ?: 1f
|
||||
val bitmap = image.bitmap
|
||||
|
||||
// Calculate max dimensions (leave padding space)
|
||||
val padding = ComfySpacing.lg * 2
|
||||
val maxWidth = availableWidth - padding
|
||||
val maxHeight = availableHeight - padding
|
||||
|
||||
// Calculate fitted size maintaining aspect ratio
|
||||
val (fittedWidth, fittedHeight) = if (bitmap != null) {
|
||||
val imageAspect = bitmap.width.toFloat() / bitmap.height.toFloat()
|
||||
val containerAspect = maxWidth / maxHeight
|
||||
|
||||
if (imageAspect > containerAspect) {
|
||||
// Image is wider - fit to width
|
||||
maxWidth to (maxWidth / imageAspect)
|
||||
} else {
|
||||
// Image is taller - fit to height
|
||||
(maxHeight * imageAspect) to maxHeight
|
||||
}
|
||||
} else {
|
||||
maxWidth to (maxWidth * 0.75f) // Default 4:3 for placeholder
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(ComfySpacing.sm)
|
||||
verticalArrangement = Arrangement.spacedBy(ComfySpacing.sm),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.widthIn(max = fittedWidth)
|
||||
.heightIn(max = fittedHeight)
|
||||
.shadow(4.dp, shape)
|
||||
.clip(shape)
|
||||
.background(MaterialTheme.comfyColors.cardBackground)
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
when {
|
||||
image.bitmap != null -> {
|
||||
bitmap != null -> {
|
||||
Image(
|
||||
bitmap = image.bitmap,
|
||||
bitmap = bitmap,
|
||||
contentDescription = image.filename,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (aspectRatio.isFinite() && aspectRatio > 0f) {
|
||||
Modifier.aspectRatio(aspectRatio)
|
||||
} else Modifier
|
||||
)
|
||||
.widthIn(max = fittedWidth)
|
||||
.heightIn(max = fittedHeight)
|
||||
)
|
||||
|
||||
// Filename badge at bottom
|
||||
@@ -141,26 +161,7 @@ private fun GalleryImageItem(
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
if (image.bitmap != null && onSetCover != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = onSetCover,
|
||||
contentPadding = PaddingValues(horizontal = ComfySpacing.md, vertical = ComfySpacing.sm)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PhotoLibrary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(ComfySpacing.xs))
|
||||
Text("设为封面")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Action buttons (reserved)
|
||||
}
|
||||
}
|
||||
|
||||
+92
-7
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.ui.components
|
||||
package moe.uni.comfyKmp.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
@@ -14,19 +14,16 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Casino
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
@@ -57,9 +54,9 @@ import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
import moe.uni.comfy_kmp.data.NodeStatus
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfy_kmp.ui.theme.comfyColors
|
||||
import moe.uni.comfyKmp.data.NodeStatus
|
||||
import moe.uni.comfyKmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfyKmp.ui.theme.comfyColors
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
@@ -235,6 +232,7 @@ enum class EditableNodeType {
|
||||
VAE_LOADER,
|
||||
LORA_LOADER,
|
||||
CLIP_TEXT_ENCODE,
|
||||
EMPTY_LATENT_IMAGE,
|
||||
OTHER
|
||||
}
|
||||
|
||||
@@ -254,6 +252,9 @@ fun detectNodeType(classType: String): EditableNodeType {
|
||||
normalized.contains("cliptextencode") ||
|
||||
normalized.contains("clip_text_encode") ||
|
||||
normalized == "clip text encode" -> EditableNodeType.CLIP_TEXT_ENCODE
|
||||
normalized.contains("emptylatentimage") ||
|
||||
normalized.contains("empty_latent_image") ||
|
||||
normalized == "empty latent image" -> EditableNodeType.EMPTY_LATENT_IMAGE
|
||||
else -> EditableNodeType.OTHER
|
||||
}
|
||||
}
|
||||
@@ -352,6 +353,7 @@ fun EditableNodeCard(
|
||||
EditableNodeType.VAE_LOADER -> "VAE"
|
||||
EditableNodeType.LORA_LOADER -> "LoRA"
|
||||
EditableNodeType.CLIP_TEXT_ENCODE -> "提示词"
|
||||
EditableNodeType.EMPTY_LATENT_IMAGE -> "空潜图"
|
||||
else -> ""
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
@@ -425,6 +427,12 @@ fun EditableNodeCard(
|
||||
onInputChange = onInputChange
|
||||
)
|
||||
}
|
||||
EditableNodeType.EMPTY_LATENT_IMAGE -> {
|
||||
EmptyLatentImageEditor(
|
||||
inputs = inputs,
|
||||
onInputChange = onInputChange
|
||||
)
|
||||
}
|
||||
EditableNodeType.OTHER -> {
|
||||
// Show read-only inputs for other node types
|
||||
val previewInputs = inputs.entries
|
||||
@@ -762,6 +770,83 @@ private fun formatTwoDecimals(value: Float): String {
|
||||
return "$whole.${fraction.toString().padStart(2, '0')}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyLatentImageEditor(
|
||||
inputs: Map<String, JsonElement>,
|
||||
onInputChange: (String, JsonElement) -> Unit
|
||||
) {
|
||||
val widthValue = inputs["width"]?.let { (it as? JsonPrimitive)?.contentOrNull } ?: ""
|
||||
val heightValue = inputs["height"]?.let { (it as? JsonPrimitive)?.contentOrNull } ?: ""
|
||||
val batchValue = inputs["batch_size"]?.let { (it as? JsonPrimitive)?.contentOrNull } ?: ""
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(ComfySpacing.md)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(ComfySpacing.md)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(ComfySpacing.xs)
|
||||
) {
|
||||
Text(
|
||||
text = "宽度",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = widthValue,
|
||||
onValueChange = { newValue ->
|
||||
newValue.toIntOrNull()?.let { onInputChange("width", JsonPrimitive(it)) }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
placeholder = { Text("如 512") }
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(ComfySpacing.xs)
|
||||
) {
|
||||
Text(
|
||||
text = "高度",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = heightValue,
|
||||
onValueChange = { newValue ->
|
||||
newValue.toIntOrNull()?.let { onInputChange("height", JsonPrimitive(it)) }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
placeholder = { Text("如 512") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(ComfySpacing.xs)) {
|
||||
Text(
|
||||
text = "批量",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = batchValue,
|
||||
onValueChange = { newValue ->
|
||||
newValue.toIntOrNull()?.let { onInputChange("batch_size", JsonPrimitive(it)) }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
placeholder = { Text("如 1") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PromptEditor(
|
||||
inputs: Map<String, JsonElement>,
|
||||
+3
-4
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.ui.components
|
||||
package moe.uni.comfyKmp.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
@@ -35,8 +34,8 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfy_kmp.ui.theme.comfyColors
|
||||
import moe.uni.comfyKmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfyKmp.ui.theme.comfyColors
|
||||
|
||||
enum class ExecutionStatus {
|
||||
IDLE,
|
||||
+42
-20
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.ui.screens
|
||||
package moe.uni.comfyKmp.ui.screens
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
@@ -7,14 +7,14 @@ import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@@ -33,14 +32,15 @@ import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import moe.uni.comfy_kmp.SystemBarsEffect
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfyKmp.SystemBarsEffect
|
||||
import moe.uni.comfyKmp.ui.theme.ComfySpacing
|
||||
import org.jetbrains.compose.resources.decodeToImageBitmap
|
||||
|
||||
data class ImagePreviewScreen(
|
||||
val filename: String,
|
||||
val bytes: ByteArray,
|
||||
val onSave: (() -> Unit)? = null
|
||||
val onSave: (() -> Unit)? = null,
|
||||
val onSetCover: (() -> Unit)? = null
|
||||
) : Screen {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
@@ -99,22 +99,44 @@ data class ImagePreviewScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (onSave != null) {
|
||||
FilledTonalIconButton(
|
||||
onClick = onSave,
|
||||
if (onSave != null || onSetCover != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(ComfySpacing.lg)
|
||||
.size(48.dp),
|
||||
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
.padding(ComfySpacing.lg),
|
||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(ComfySpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Save,
|
||||
contentDescription = "保存"
|
||||
)
|
||||
if (onSetCover != null) {
|
||||
FilledTonalIconButton(
|
||||
onClick = onSetCover,
|
||||
modifier = Modifier.size(48.dp),
|
||||
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PhotoLibrary,
|
||||
contentDescription = "设为封面"
|
||||
)
|
||||
}
|
||||
}
|
||||
if (onSave != null) {
|
||||
FilledTonalIconButton(
|
||||
onClick = onSave,
|
||||
modifier = Modifier.size(48.dp),
|
||||
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Save,
|
||||
contentDescription = "保存"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-16
@@ -1,6 +1,6 @@
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package moe.uni.comfy_kmp.ui.screens
|
||||
package moe.uni.comfyKmp.ui.screens
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
@@ -30,7 +30,6 @@ import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Dns
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.RadioButtonChecked
|
||||
import androidx.compose.material.icons.filled.RadioButtonUnchecked
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
@@ -40,7 +39,6 @@ import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -65,13 +63,13 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import moe.uni.comfy_kmp.data.ServerConfig
|
||||
import moe.uni.comfy_kmp.di.LocalAppContainer
|
||||
import moe.uni.comfy_kmp.storage.generateId
|
||||
import moe.uni.comfy_kmp.ui.components.ComfyDialog
|
||||
import moe.uni.comfy_kmp.ui.components.GlassCard
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfy_kmp.ui.theme.comfyColors
|
||||
import moe.uni.comfyKmp.data.ServerConfig
|
||||
import moe.uni.comfyKmp.di.LocalAppContainer
|
||||
import moe.uni.comfyKmp.storage.generateId
|
||||
import moe.uni.comfyKmp.ui.components.ComfyDialog
|
||||
import moe.uni.comfyKmp.ui.components.GlassCard
|
||||
import moe.uni.comfyKmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfyKmp.ui.theme.comfyColors
|
||||
|
||||
class ServerScreen : Screen {
|
||||
@Composable
|
||||
@@ -106,11 +104,6 @@ class ServerScreen : Screen {
|
||||
"Comfy KMP",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Text(
|
||||
text = "ComfyUI 移动客户端",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
@@ -228,7 +221,7 @@ class ServerScreen : Screen {
|
||||
}
|
||||
|
||||
private class ServerScreenModel(
|
||||
private val repository: moe.uni.comfy_kmp.storage.ServerRepository
|
||||
private val repository: moe.uni.comfyKmp.storage.ServerRepository
|
||||
) : ScreenModel {
|
||||
var servers by mutableStateOf(repository.getServers())
|
||||
private set
|
||||
+26
-23
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.ui.screens
|
||||
package moe.uni.comfyKmp.ui.screens
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
@@ -31,9 +31,7 @@ import androidx.compose.material.icons.automirrored.filled.Assignment
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
@@ -45,10 +43,10 @@ import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -58,7 +56,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -67,15 +64,15 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import moe.uni.comfy_kmp.data.WorkflowEntity
|
||||
import moe.uni.comfy_kmp.data.loadCoverImageBytes
|
||||
import moe.uni.comfy_kmp.di.LocalAppContainer
|
||||
import moe.uni.comfy_kmp.isFilePickerSupported
|
||||
import moe.uni.comfy_kmp.rememberFilePickerLauncher
|
||||
import moe.uni.comfy_kmp.storage.generateId
|
||||
import moe.uni.comfy_kmp.ui.components.ComfyDialog
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfy_kmp.ui.theme.comfyColors
|
||||
import moe.uni.comfyKmp.data.WorkflowEntity
|
||||
import moe.uni.comfyKmp.data.loadCoverImageBytes
|
||||
import moe.uni.comfyKmp.di.LocalAppContainer
|
||||
import moe.uni.comfyKmp.isFilePickerSupported
|
||||
import moe.uni.comfyKmp.rememberFilePickerLauncher
|
||||
import moe.uni.comfyKmp.storage.generateId
|
||||
import moe.uni.comfyKmp.ui.components.ComfyDialog
|
||||
import moe.uni.comfyKmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfyKmp.ui.theme.comfyColors
|
||||
import org.jetbrains.compose.resources.decodeToImageBitmap
|
||||
import kotlin.time.Clock
|
||||
|
||||
@@ -87,6 +84,10 @@ data class WorkflowListScreen(val serverId: String) : Screen {
|
||||
val container = LocalAppContainer.current
|
||||
val model = rememberScreenModel { WorkflowListScreenModel(serverId, container.workflowRepository) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
model.refresh()
|
||||
}
|
||||
|
||||
var showImport by remember { mutableStateOf(false) }
|
||||
|
||||
if (showImport) {
|
||||
@@ -194,11 +195,15 @@ data class WorkflowListScreen(val serverId: String) : Screen {
|
||||
|
||||
private class WorkflowListScreenModel(
|
||||
private val serverId: String,
|
||||
private val repository: moe.uni.comfy_kmp.storage.WorkflowRepository
|
||||
private val repository: moe.uni.comfyKmp.storage.WorkflowRepository
|
||||
) : ScreenModel {
|
||||
var workflows by mutableStateOf(repository.getWorkflows(serverId))
|
||||
private set
|
||||
|
||||
fun refresh() {
|
||||
workflows = repository.getWorkflows(serverId)
|
||||
}
|
||||
|
||||
fun addWorkflow(name: String, json: String) {
|
||||
val workflow = WorkflowEntity(
|
||||
id = generateId("workflow"),
|
||||
@@ -208,12 +213,12 @@ private class WorkflowListScreenModel(
|
||||
updatedAt = Clock.System.now().epochSeconds
|
||||
)
|
||||
repository.upsertWorkflow(workflow)
|
||||
workflows = repository.getWorkflows(serverId)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun deleteWorkflow(id: String) {
|
||||
repository.deleteWorkflow(id)
|
||||
workflows = repository.getWorkflows(serverId)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,18 +330,17 @@ private fun WorkflowCard(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.85f)
|
||||
.shadow(4.dp, shape)
|
||||
.clip(shape)
|
||||
.background(comfyColors.cardBackground)
|
||||
.clickable(onClick = onRun)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Cover image area (3/4)
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
// Cover image area (4:3)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(4f)
|
||||
.aspectRatio(4f / 3f)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
if (coverBitmap != null) {
|
||||
@@ -372,11 +376,10 @@ private fun WorkflowCard(
|
||||
}
|
||||
}
|
||||
|
||||
// Info and actions area (1/4)
|
||||
// Info and actions area
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.background(comfyColors.cardBackground)
|
||||
.padding(ComfySpacing.md),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
+90
-33
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.ui.screens
|
||||
package moe.uni.comfyKmp.ui.screens
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -68,34 +68,35 @@ import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import moe.uni.comfy_kmp.data.ComfyJson
|
||||
import moe.uni.comfy_kmp.data.ImageRef
|
||||
import moe.uni.comfy_kmp.data.NodeExecutionState
|
||||
import moe.uni.comfy_kmp.data.NodeStatus
|
||||
import moe.uni.comfy_kmp.data.PromptRequest
|
||||
import moe.uni.comfy_kmp.data.extractImagesFromHistory
|
||||
import moe.uni.comfy_kmp.data.extractPromptObject
|
||||
import moe.uni.comfy_kmp.data.parsePromptNodes
|
||||
import moe.uni.comfy_kmp.data.saveCoverImage
|
||||
import moe.uni.comfy_kmp.data.saveImageToGallery
|
||||
import moe.uni.comfy_kmp.data.updateNodeInput
|
||||
import moe.uni.comfy_kmp.di.LocalAppContainer
|
||||
import moe.uni.comfy_kmp.network.ComfyApiClient
|
||||
import moe.uni.comfy_kmp.network.ComfyWebSocketClient
|
||||
import moe.uni.comfy_kmp.storage.ServerRepository
|
||||
import moe.uni.comfy_kmp.storage.WorkflowRepository
|
||||
import moe.uni.comfy_kmp.storage.generateId
|
||||
import moe.uni.comfy_kmp.ui.components.EditableNodeCard
|
||||
import moe.uni.comfy_kmp.ui.components.ExecutionStatus
|
||||
import moe.uni.comfy_kmp.ui.components.ExecutionStatusBar
|
||||
import moe.uni.comfy_kmp.ui.components.GalleryImage
|
||||
import moe.uni.comfy_kmp.ui.components.ImageGallery
|
||||
import moe.uni.comfy_kmp.ui.components.detectNodeType
|
||||
import moe.uni.comfy_kmp.ui.components.getModelFolder
|
||||
import moe.uni.comfy_kmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfy_kmp.ui.theme.comfyColors
|
||||
import moe.uni.comfyKmp.data.ComfyJson
|
||||
import moe.uni.comfyKmp.data.ImageRef
|
||||
import moe.uni.comfyKmp.data.NodeExecutionState
|
||||
import moe.uni.comfyKmp.data.NodeStatus
|
||||
import moe.uni.comfyKmp.data.PromptRequest
|
||||
import moe.uni.comfyKmp.data.extractImagesFromHistory
|
||||
import moe.uni.comfyKmp.data.extractPromptObject
|
||||
import moe.uni.comfyKmp.data.parsePromptNodes
|
||||
import moe.uni.comfyKmp.data.saveCoverImage
|
||||
import moe.uni.comfyKmp.data.saveImageToGallery
|
||||
import moe.uni.comfyKmp.data.updateNodeInput
|
||||
import moe.uni.comfyKmp.di.LocalAppContainer
|
||||
import moe.uni.comfyKmp.network.ComfyApiClient
|
||||
import moe.uni.comfyKmp.network.ComfyWebSocketClient
|
||||
import moe.uni.comfyKmp.network.WsConnectionStatus
|
||||
import moe.uni.comfyKmp.storage.ServerRepository
|
||||
import moe.uni.comfyKmp.storage.WorkflowRepository
|
||||
import moe.uni.comfyKmp.storage.generateId
|
||||
import moe.uni.comfyKmp.ui.components.EditableNodeType
|
||||
import moe.uni.comfyKmp.ui.components.EditableNodeCard
|
||||
import moe.uni.comfyKmp.ui.components.ExecutionStatus
|
||||
import moe.uni.comfyKmp.ui.components.ExecutionStatusBar
|
||||
import moe.uni.comfyKmp.ui.components.GalleryImage
|
||||
import moe.uni.comfyKmp.ui.components.ImageGallery
|
||||
import moe.uni.comfyKmp.ui.components.detectNodeType
|
||||
import moe.uni.comfyKmp.ui.components.getModelFolder
|
||||
import moe.uni.comfyKmp.ui.theme.ComfySpacing
|
||||
import moe.uni.comfyKmp.ui.theme.comfyColors
|
||||
import org.jetbrains.compose.resources.decodeToImageBitmap
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Clock
|
||||
@@ -222,13 +223,13 @@ data class WorkflowRunScreen(val workflowId: String) : Screen {
|
||||
ImagePreviewScreen(
|
||||
preview.filename,
|
||||
preview.bytes,
|
||||
onSave = { model.saveImage(index) }
|
||||
onSave = { model.saveImage(index) },
|
||||
onSetCover = { model.setCoverImage(index) }
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onSaveClick = { index -> model.saveImage(index) },
|
||||
onSetCoverClick = { index -> model.setCoverImage(index) },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(contentPadding)
|
||||
@@ -424,6 +425,8 @@ private class WorkflowRunScreenModel(
|
||||
private var imageBytes by mutableStateOf<Map<String, ByteArray>>(emptyMap())
|
||||
private var isLoadingImages by mutableStateOf(false)
|
||||
private var saveJob: Job? = null
|
||||
private var wsEventsJob: Job? = null
|
||||
private var wsStateJob: Job? = null
|
||||
|
||||
val galleryImages: List<GalleryImage>
|
||||
get() = imageRefs.map { ref ->
|
||||
@@ -445,6 +448,10 @@ private class WorkflowRunScreenModel(
|
||||
}
|
||||
|
||||
init {
|
||||
randomSeedNodes = nodeStates
|
||||
.filter { detectNodeType(it.classType) == EditableNodeType.KSAMPLER }
|
||||
.map { it.nodeId }
|
||||
.toSet()
|
||||
preloadModelLists()
|
||||
}
|
||||
|
||||
@@ -555,13 +562,63 @@ private class WorkflowRunScreenModel(
|
||||
executionStatus = ExecutionStatus.CONNECTING
|
||||
statusText = "正在连接..."
|
||||
|
||||
screenModelScope.launch {
|
||||
wsClient.disconnect()
|
||||
wsClient.connect(baseUrl, clientId)
|
||||
wsEventsJob?.cancel()
|
||||
wsStateJob?.cancel()
|
||||
|
||||
wsEventsJob = screenModelScope.launch {
|
||||
wsClient.events.collectLatest { msg ->
|
||||
handleWebSocketMessage(msg.type, msg.data)
|
||||
}
|
||||
}
|
||||
|
||||
wsStateJob = screenModelScope.launch {
|
||||
wsClient.connectionState.collectLatest { state ->
|
||||
when (state.status) {
|
||||
WsConnectionStatus.CONNECTING -> {
|
||||
executionStatus = ExecutionStatus.CONNECTING
|
||||
statusText = "正在连接..."
|
||||
}
|
||||
WsConnectionStatus.CONNECTED -> {
|
||||
if (!running) {
|
||||
executionStatus = ExecutionStatus.IDLE
|
||||
statusText = "已连接"
|
||||
}
|
||||
}
|
||||
WsConnectionStatus.RECONNECTING -> {
|
||||
executionStatus = ExecutionStatus.CONNECTING
|
||||
statusText = "连接断开,正在重连(${state.attempt}/${state.maxAttempts})"
|
||||
}
|
||||
WsConnectionStatus.FAILED -> {
|
||||
executionStatus = ExecutionStatus.ERROR
|
||||
statusText = "重连失败(已重试${state.maxAttempts}次)"
|
||||
}
|
||||
WsConnectionStatus.DISCONNECTED -> {
|
||||
if (!running) {
|
||||
executionStatus = ExecutionStatus.IDLE
|
||||
statusText = "已断开"
|
||||
}
|
||||
}
|
||||
WsConnectionStatus.IDLE -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenModelScope.launch {
|
||||
wsClient.disconnect()
|
||||
wsClient.connect(baseUrl, clientId)
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
wsEventsJob?.cancel()
|
||||
wsEventsJob = null
|
||||
wsStateJob?.cancel()
|
||||
wsStateJob = null
|
||||
wsClient.disconnectAsync()
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
disconnect()
|
||||
}
|
||||
|
||||
private fun handleWebSocketMessage(type: String, data: JsonObject?) {
|
||||
+57
-59
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.ui.theme
|
||||
package moe.uni.comfyKmp.ui.theme
|
||||
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -11,61 +11,59 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Light Mode Colors
|
||||
private val LightPrimary = Color(0xFF0EA5E9) // 电光蓝
|
||||
// Light Mode Colors - Blue 蓝色系
|
||||
private val LightPrimary = Color(0xFF2563EB) // Blue 600
|
||||
private val LightOnPrimary = Color(0xFFFFFFFF)
|
||||
private val LightPrimaryContainer = Color(0xFFE0F2FE)
|
||||
private val LightOnPrimaryContainer = Color(0xFF0C4A6E)
|
||||
private val LightSecondary = Color(0xFF6366F1) // 靛蓝
|
||||
private val LightPrimaryContainer = Color(0xFFDBEAFE) // Blue 100
|
||||
private val LightOnPrimaryContainer = Color(0xFF1E3A8A) // Blue 900
|
||||
private val LightSecondary = Color(0xFF3B82F6) // Blue 500
|
||||
private val LightOnSecondary = Color.White
|
||||
private val LightSecondaryContainer = Color(0xFFE0E7FF)
|
||||
private val LightOnSecondaryContainer = Color(0xFF3730A3)
|
||||
private val LightTertiary = Color(0xFF10B981) // 翡翠绿
|
||||
private val LightSecondaryContainer = Color(0xFFBFDBFE) // Blue 200
|
||||
private val LightOnSecondaryContainer = Color(0xFF1E40AF) // Blue 800
|
||||
private val LightTertiary = Color(0xFF8B5CF6) // Violet 500
|
||||
private val LightOnTertiary = Color.White
|
||||
private val LightTertiaryContainer = Color(0xFFD1FAE5)
|
||||
private val LightOnTertiaryContainer = Color(0xFF065F46)
|
||||
private val LightBackground = Color(0xFFF5F5F7)
|
||||
private val LightOnBackground = Color(0xFF1A1A1A)
|
||||
private val LightTertiaryContainer = Color(0xFFEDE9FE) // Violet 100
|
||||
private val LightOnTertiaryContainer = Color(0xFF5B21B6) // Violet 800
|
||||
private val LightBackground = Color(0xFFF8FAFC) // Slate 50
|
||||
private val LightOnBackground = Color(0xFF1E293B) // Slate 800
|
||||
private val LightSurface = Color(0xFFFFFFFF)
|
||||
private val LightOnSurface = Color(0xFF1A1A1A)
|
||||
private val LightSurfaceVariant = Color(0xFFE8E8ED)
|
||||
private val LightOnSurfaceVariant = Color(0xFF4A4A52)
|
||||
private val LightOutline = Color(0xFFD1D5DB)
|
||||
private val LightOutlineVariant = Color(0xFFE5E7EB)
|
||||
private val LightError = Color(0xFFEF4444)
|
||||
private val LightOnSurface = Color(0xFF1E293B) // Slate 800
|
||||
private val LightSurfaceVariant = Color(0xFFF1F5F9) // Slate 100
|
||||
private val LightOnSurfaceVariant = Color(0xFF475569) // Slate 600
|
||||
private val LightOutline = Color(0xFFCBD5E1) // Slate 300
|
||||
private val LightOutlineVariant = Color(0xFFE2E8F0) // Slate 200
|
||||
private val LightError = Color(0xFFDC2626) // Red 600
|
||||
private val LightOnError = Color.White
|
||||
|
||||
// Dark Mode Colors
|
||||
private val DarkPrimary = Color(0xFF38BDF8) // 电光蓝
|
||||
private val DarkOnPrimary = Color(0xFF0C1929)
|
||||
private val DarkPrimaryContainer = Color(0xFF0369A1)
|
||||
private val DarkOnPrimaryContainer = Color(0xFFBAE6FD)
|
||||
private val DarkSecondary = Color(0xFFA78BFA) // 紫罗兰
|
||||
private val DarkOnSecondary = Color(0xFF1E1B4B)
|
||||
private val DarkSecondaryContainer = Color(0xFF4C1D95)
|
||||
private val DarkOnSecondaryContainer = Color(0xFFEDE9FE)
|
||||
private val DarkTertiary = Color(0xFF34D399) // 翡翠绿
|
||||
private val DarkOnTertiary = Color(0xFF052E16)
|
||||
private val DarkTertiaryContainer = Color(0xFF065F46)
|
||||
private val DarkOnTertiaryContainer = Color(0xFFA7F3D0)
|
||||
private val DarkBackground = Color(0xFF0D0D12)
|
||||
private val DarkOnBackground = Color(0xFFE8E8ED)
|
||||
private val DarkSurface = Color(0xFF1A1A24)
|
||||
private val DarkOnSurface = Color(0xFFE8E8ED)
|
||||
private val DarkSurfaceVariant = Color(0xFF2A2A36)
|
||||
private val DarkOnSurfaceVariant = Color(0xFFA1A1AA)
|
||||
private val DarkOutline = Color(0xFF3F3F46)
|
||||
private val DarkOutlineVariant = Color(0xFF27272A)
|
||||
private val DarkError = Color(0xFFF87171)
|
||||
private val DarkOnError = Color(0xFF1A0A0A)
|
||||
// Dark Mode Colors - Blue 蓝色系
|
||||
private val DarkPrimary = Color(0xFF60A5FA) // Blue 400
|
||||
private val DarkOnPrimary = Color(0xFF1E3A8A) // Blue 900
|
||||
private val DarkPrimaryContainer = Color(0xFF1E40AF) // Blue 800
|
||||
private val DarkOnPrimaryContainer = Color(0xFFDBEAFE) // Blue 100
|
||||
private val DarkSecondary = Color(0xFF93C5FD) // Blue 300
|
||||
private val DarkOnSecondary = Color(0xFF1E3A8A) // Blue 900
|
||||
private val DarkSecondaryContainer = Color(0xFF1D4ED8) // Blue 700
|
||||
private val DarkOnSecondaryContainer = Color(0xFFBFDBFE) // Blue 200
|
||||
private val DarkTertiary = Color(0xFFA78BFA) // Violet 400
|
||||
private val DarkOnTertiary = Color(0xFF4C1D95) // Violet 900
|
||||
private val DarkTertiaryContainer = Color(0xFF6D28D9) // Violet 700
|
||||
private val DarkOnTertiaryContainer = Color(0xFFEDE9FE) // Violet 100
|
||||
private val DarkBackground = Color(0xFF0F172A) // Slate 900
|
||||
private val DarkOnBackground = Color(0xFFE2E8F0) // Slate 200
|
||||
private val DarkSurface = Color(0xFF1E293B) // Slate 800
|
||||
private val DarkOnSurface = Color(0xFFE2E8F0) // Slate 200
|
||||
private val DarkSurfaceVariant = Color(0xFF334155) // Slate 700
|
||||
private val DarkOnSurfaceVariant = Color(0xFF94A3B8) // Slate 400
|
||||
private val DarkOutline = Color(0xFF475569) // Slate 600
|
||||
private val DarkOutlineVariant = Color(0xFF334155) // Slate 700
|
||||
private val DarkError = Color(0xFFF87171) // Red 400
|
||||
private val DarkOnError = Color(0xFF7F1D1D) // Red 900
|
||||
|
||||
private val LightColors = lightColorScheme(
|
||||
primary = LightPrimary,
|
||||
@@ -240,14 +238,14 @@ private val LightExtendedColors = ComfyExtendedColors(
|
||||
onWarning = Color(0xFF1A1A1A),
|
||||
warningContainer = Color(0xFFFEF3C7),
|
||||
cardBackground = Color.White,
|
||||
cardBorder = Color(0xFFE5E7EB),
|
||||
shimmerBase = Color(0xFFE5E7EB),
|
||||
shimmerHighlight = Color(0xFFF3F4F6),
|
||||
gradientStart = Color(0xFFF5F5F7),
|
||||
gradientEnd = Color(0xFFE8E8ED),
|
||||
nodeRunning = Color(0xFF0EA5E9),
|
||||
cardBorder = Color(0xFFE2E8F0), // Slate 200
|
||||
shimmerBase = Color(0xFFE2E8F0), // Slate 200
|
||||
shimmerHighlight = Color(0xFFF1F5F9), // Slate 100
|
||||
gradientStart = Color(0xFFF8FAFC), // Slate 50
|
||||
gradientEnd = Color(0xFFF1F5F9), // Slate 100
|
||||
nodeRunning = Color(0xFF2563EB), // Blue 600
|
||||
nodeCompleted = Color(0xFF10B981),
|
||||
nodePending = Color(0xFF9CA3AF)
|
||||
nodePending = Color(0xFF94A3B8) // Slate 400
|
||||
)
|
||||
|
||||
private val DarkExtendedColors = ComfyExtendedColors(
|
||||
@@ -257,15 +255,15 @@ private val DarkExtendedColors = ComfyExtendedColors(
|
||||
warning = Color(0xFFFBBF24),
|
||||
onWarning = Color(0xFF1A1A1A),
|
||||
warningContainer = Color(0xFF78350F),
|
||||
cardBackground = Color(0xFF1F1F2A),
|
||||
cardBorder = Color(0xFF3F3F46),
|
||||
shimmerBase = Color(0xFF27272A),
|
||||
shimmerHighlight = Color(0xFF3F3F46),
|
||||
gradientStart = Color(0xFF0D0D12),
|
||||
gradientEnd = Color(0xFF1A1A24),
|
||||
nodeRunning = Color(0xFF38BDF8),
|
||||
cardBackground = Color(0xFF1E293B), // Slate 800
|
||||
cardBorder = Color(0xFF475569), // Slate 600
|
||||
shimmerBase = Color(0xFF334155), // Slate 700
|
||||
shimmerHighlight = Color(0xFF475569), // Slate 600
|
||||
gradientStart = Color(0xFF0F172A), // Slate 900
|
||||
gradientEnd = Color(0xFF1E293B), // Slate 800
|
||||
nodeRunning = Color(0xFF60A5FA), // Blue 400
|
||||
nodeCompleted = Color(0xFF34D399),
|
||||
nodePending = Color(0xFF6B7280)
|
||||
nodePending = Color(0xFF64748B) // Slate 500
|
||||
)
|
||||
|
||||
val LocalComfyColors = staticCompositionLocalOf { LightExtendedColors }
|
||||
@@ -1,76 +0,0 @@
|
||||
package moe.uni.comfy_kmp.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.websocket.webSocket
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.utils.io.errors.IOException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import moe.uni.comfy_kmp.data.ComfyJson
|
||||
import moe.uni.comfy_kmp.data.WsMessage
|
||||
|
||||
class ComfyWebSocketClient(
|
||||
private val httpClient: HttpClient
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val _events = MutableSharedFlow<WsMessage>(extraBufferCapacity = 64)
|
||||
val events: SharedFlow<WsMessage> = _events
|
||||
|
||||
private var session: WebSocketSession? = null
|
||||
|
||||
fun connect(baseUrl: String, clientId: String) {
|
||||
if (session != null) return
|
||||
val wsUrl = buildWsUrl(baseUrl, clientId)
|
||||
scope.launch {
|
||||
try {
|
||||
httpClient.webSocket(wsUrl) {
|
||||
session = this
|
||||
try {
|
||||
for (frame in incoming) {
|
||||
if (frame is Frame.Text) {
|
||||
val element = ComfyJson.parseToJsonElement(frame.readText())
|
||||
val obj = element.jsonObject
|
||||
val type = obj["type"]?.jsonPrimitive?.content ?: "unknown"
|
||||
val data = obj["data"] as? JsonObject
|
||||
_events.tryEmit(WsMessage(type = type, data = data))
|
||||
}
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
// 后台/断网时底层 socket 关闭会抛异常,这里忽略即可
|
||||
} finally {
|
||||
session = null
|
||||
}
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
session = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disconnect() {
|
||||
session?.close()
|
||||
session = null
|
||||
}
|
||||
|
||||
fun close() {
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun buildWsUrl(baseUrl: String, clientId: String): String {
|
||||
val trimmed = baseUrl.trimEnd('/')
|
||||
val scheme = if (trimmed.startsWith("https://")) "wss://" else "ws://"
|
||||
val host = trimmed.removePrefix("https://").removePrefix("http://")
|
||||
return "${scheme}${host}/ws?clientId=$clientId"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import androidx.compose.ui.window.ComposeUIViewController
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import platform.UIKit.UIDevice
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp
|
||||
package moe.uni.comfyKmp
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.convert
|
||||
@@ -11,7 +11,6 @@ import platform.Foundation.NSCachesDirectory
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
import platform.Foundation.URLByAppendingPathComponent
|
||||
import platform.Foundation.dataWithBytes
|
||||
import platform.UIKit.UIImage
|
||||
import platform.UIKit.UIImageJPEGRepresentation
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||
|
||||
package moe.uni.comfy_kmp.data
|
||||
package moe.uni.comfyKmp.data
|
||||
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.usePinned
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package moe.uni.comfy_kmp.network
|
||||
package moe.uni.comfyKmp.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.darwin.Darwin
|
||||
@@ -1,7 +1,7 @@
|
||||
TEAM_ID=
|
||||
|
||||
PRODUCT_NAME=comfy_kmp
|
||||
PRODUCT_BUNDLE_IDENTIFIER=moe.uni.comfy_kmp.comfy_kmp$(TEAM_ID)
|
||||
PRODUCT_NAME=comfyKmp
|
||||
PRODUCT_BUNDLE_IDENTIFIER=moe.uni.comfyKmp.comfyKmp$(TEAM_ID)
|
||||
|
||||
CURRENT_PROJECT_VERSION=1
|
||||
MARKETING_VERSION=1.0
|
||||
@@ -7,7 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
338A2ACE269EA4A9809609C8 /* comfy_kmp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = comfy_kmp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
338A2ACE269EA4A9809609C8 /* comfyKmp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = comfyKmp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
@@ -59,7 +59,7 @@
|
||||
657B04DAB4CB0FD124755A63 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
338A2ACE269EA4A9809609C8 /* comfy_kmp.app */,
|
||||
338A2ACE269EA4A9809609C8 /* comfyKmp.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -87,7 +87,7 @@
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = iosApp;
|
||||
productReference = 338A2ACE269EA4A9809609C8 /* comfy_kmp.app */;
|
||||
productReference = 338A2ACE269EA4A9809609C8 /* comfyKmp.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
rootProject.name = "comfy_kmp"
|
||||
rootProject.name = "comfyKmp"
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
||||
pluginManagement {
|
||||
|
||||
Reference in New Issue
Block a user