fix: bugs

This commit is contained in:
AnranYus
2025-10-06 14:01:49 +08:00
parent 2afdf55341
commit 342a1a370d
29 changed files with 727 additions and 465 deletions
+5 -1
View File
@@ -31,6 +31,7 @@ kotlin {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.ktor.client.okhttp)
implementation("io.ktor:ktor-client-android:3.3.0")
}
commonMain.dependencies {
implementation(compose.runtime)
@@ -47,13 +48,16 @@ kotlin {
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.websockets)
api(libs.androidx.lifecycle.viewmodel)
implementation(compose.materialIconsExtended)
implementation("co.touchlab:kermit:2.0.4") //Add latest version
implementation("br.com.devsrsouza.compose.icons:font-awesome:1.1.1")
implementation("io.coil-kt.coil3:coil-compose:3.3.0")
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:3.3.0")
implementation(libs.ktor.client.darwin)
}
}
@@ -21,3 +21,4 @@ actual object KeyValueStore {
}
@@ -17,3 +17,4 @@ actual fun FileImportFab(onImported: (ComfyPrompt) -> Unit, onError: (String) ->
}
@@ -8,3 +8,4 @@ actual fun decodeImage(bytes: ByteArray): ImageBitmap? =
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)?.asImageBitmap()
@@ -4,3 +4,4 @@
</network-security-config>
@@ -10,7 +10,7 @@ import kotlinx.serialization.json.JsonElement
@Serializable
data class ComfyNode(
@SerialName("class_type") val classType: String,
val inputs: Map<String, JsonElement> = emptyMap(),
var inputs: Map<String, JsonElement> = emptyMap(),
@SerialName("_meta") val meta: ComfyNodeMeta? = null
)
@@ -27,8 +27,8 @@ fun extractImageRefsFromHistoryElement(root: JsonElement): List<ImageRef> {
is JsonObject -> {
// 命中 images 字段
element["images"]?.let { imagesEl ->
val arr: JsonArray? = imagesEl as? JsonArray ?: imagesEl.jsonArray
arr?.forEach { imgEl ->
val arr: JsonArray = imagesEl as? JsonArray ?: imagesEl.jsonArray
arr.forEach { imgEl ->
val obj: JsonObject? = imgEl as? JsonObject ?: runCatching { imgEl.jsonObject }.getOrNull()
if (obj != null) {
val filename = runCatching { obj["filename"]?.jsonPrimitive?.content }.getOrNull()
@@ -35,6 +35,9 @@ class ComfyApi(private val baseUrl: String, private val httpClient: HttpClient)
suspend fun getHistory(promptId: String): Map<String, JsonElement> =
httpClient.get("$baseUrl/history/$promptId").body()
suspend fun getHistory(size: Int): Map<String, JsonElement> =
httpClient.get("$baseUrl/history?max_items=${size}").body()
suspend fun fetchImage(
filename: String,
subfolder: String? = null,
@@ -4,6 +4,7 @@ import io.ktor.client.HttpClient
import io.ktor.client.plugins.websocket.webSocket
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.yield
import kotlinx.serialization.json.Json
@@ -17,7 +18,7 @@ import kotlin.coroutines.cancellation.CancellationException
data class ProgressUpdate(
val fraction: Float?,
val message: String?
val message: String?,
)
class ComfyWsClient(private val client: HttpClient) {
@@ -33,6 +34,10 @@ class ComfyWsClient(private val client: HttpClient) {
return "$wsBase/ws?clientId=$clientId"
}
fun close(){
client.close()
}
suspend fun listen(
baseUrl: String,
clientId: String,
@@ -40,7 +45,12 @@ class ComfyWsClient(private val client: HttpClient) {
onImages: (List<ImageRef>) -> Unit = {}
) {
val url = toWsUrl(baseUrl, clientId)
var attempt = 0
while (true) {
try {
client.webSocket(urlString = url) {
// reset backoff on successful connect
attempt = 0
try {
for (frame in incoming) {
val text = (frame as? Frame.Text)?.readText() ?: continue
@@ -57,9 +67,12 @@ class ComfyWsClient(private val client: HttpClient) {
}
"executing" -> {
val node = obj["data"]?.jsonObject?.get("node")?.jsonPrimitive?.content
onUpdate(ProgressUpdate(null, "执行节点: ${node ?: ""}"))
if (node != null && node != "null") {
onUpdate(ProgressUpdate(null, "执行节点: ${node}"))
}
}
"executed" -> {
onUpdate(ProgressUpdate(null, "success"))
val data: JsonElement = obj["data"] ?: continue
val refs = extractImageRefsFromHistoryElement(data)
if (refs.isNotEmpty()) onImages(refs)
@@ -71,7 +84,19 @@ class ComfyWsClient(private val client: HttpClient) {
if (!this@webSocket.isActive) break
yield()
}
} catch (e: CancellationException) {
throw e
}
}
} catch (_: CancellationException) {
break
} catch (_: Throwable) {
// 连接错误或读写错误:指数退避重连
val backoffSeconds = (1 shl attempt).coerceAtMost(32)
onUpdate(ProgressUpdate(null, "连接断开,${backoffSeconds}s 后重连…"))
attempt = (attempt + 1).coerceAtMost(5)
delay(backoffSeconds * 1000L)
continue
}
}
}
@@ -1,15 +1,10 @@
package moe.uni.comfy.comfyuikmp.data.repository
import co.touchlab.kermit.Logger
import io.ktor.client.HttpClient
import moe.uni.comfy.comfyuikmp.data.model.ComfyPrompt
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
import moe.uni.comfy.comfyuikmp.data.model.extractImageRefsFromHistoryElement
import moe.uni.comfy.comfyuikmp.data.network.ComfyApi
import moe.uni.comfy.comfyuikmp.data.network.createDefaultHttpClient
import kotlinx.serialization.json.JsonElement
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
class ComfyRepository(
private val settingsRepository: SettingsRepository,
@@ -17,7 +12,7 @@ class ComfyRepository(
) {
private fun apiOrNull(): ComfyApi? {
val base = settingsRepository.getBaseUrl() ?: return null
return ComfyApi(baseUrl = base.trimEnd('/'), httpClient = httpClient)
return ComfyApi(baseUrl = base, httpClient = httpClient)
}
suspend fun submit(prompt: ComfyPrompt, clientId: String? = null) =
@@ -26,31 +21,11 @@ class ComfyRepository(
suspend fun queue() = apiOrNull()?.getQueue()
suspend fun history(promptId: String) = apiOrNull()?.getHistory(promptId)
suspend fun history(size: Int) = apiOrNull()?.getHistory(size)
suspend fun image(filename: String, subfolder: String? = null, type: String? = null) =
apiOrNull()?.fetchImage(filename, subfolder, type)
/**
* 提交并轮询直到历史中出现图片引用或超时。
*/
suspend fun runAndWaitImages(prompt: ComfyPrompt, clientId: String? = null, timeoutMs: Long = 120_000): List<ImageRef> {
val api = apiOrNull() ?: return emptyList()
val submit = api.submitPrompt(prompt, clientId)
Logger.i { "Submit result: $submit." }
return withTimeout(timeoutMs) {
while (true) {
val hist: Map<String, JsonElement> = api.getHistory(submit.promptId)
Logger.i { "History result: $hist." }
val merged = hist.values.firstOrNull()
if (merged != null) {
val refs = extractImageRefsFromHistoryElement(merged)
if (refs.isNotEmpty()) return@withTimeout refs
}
delay(1000)
}
emptyList()
}
}
}
@@ -9,3 +9,4 @@ class SettingsRepository {
}
@@ -11,3 +11,4 @@ expect object KeyValueStore {
}
@@ -1,97 +1,90 @@
package moe.uni.comfy.comfyuikmp.ui
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import moe.uni.comfy.comfyuikmp.ui.navigation.Dest
import moe.uni.comfy.comfyuikmp.ui.settings.SettingsScreen
import moe.uni.comfy.comfyuikmp.ui.importer.ImportScreen
import moe.uni.comfy.comfyuikmp.ui.editor.EditorScreen
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import moe.uni.comfy.comfyuikmp.data.model.ComfyPrompt
import moe.uni.comfy.comfyuikmp.ui.gallery.GalleryScreen
import moe.uni.comfy.comfyuikmp.util.decodeImage
import androidx.compose.ui.graphics.ImageBitmap
import kotlinx.serialization.json.JsonPrimitive
import moe.uni.comfy.comfyuikmp.di.AppGraph
import androidx.compose.material3.ExtendedFloatingActionButton
import co.touchlab.kermit.Logger
import moe.uni.comfy.comfyuikmp.ui.gallery.GalleryScreen
import moe.uni.comfy.comfyuikmp.ui.generate.GenerateScreen
import moe.uni.comfy.comfyuikmp.ui.home.HomeScreen
import moe.uni.comfy.comfyuikmp.data.repository.WorkflowItem
import moe.uni.comfy.comfyuikmp.data.network.ComfyWsClient
import moe.uni.comfy.comfyuikmp.data.network.ProgressUpdate
import moe.uni.comfy.comfyuikmp.data.network.createDefaultHttpClient
import moe.uni.comfy.comfyuikmp.ui.importer.ImportScreen
import moe.uni.comfy.comfyuikmp.ui.navigation.Dest
import moe.uni.comfy.comfyuikmp.ui.navigation.rememberNavController
import moe.uni.comfy.comfyuikmp.ui.settings.SettingsScreen
import moe.uni.comfy.comfyuikmp.util.mainViewModelFactory
import kotlin.random.Random
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
@Composable
fun AppRoot() {
fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory)) {
val hasSaved = remember { AppGraph.workflows.list().isNotEmpty() }
val current: MutableState<Dest> = remember { mutableStateOf(if (hasSaved) Dest.Home else Dest.Settings) }
val startDest = if (hasSaved) Dest.Home else Dest.Settings
val navController = rememberNavController(startDest)
MaterialTheme {
val snackbar = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
var promptState by remember { mutableStateOf<ComfyPrompt?>(null) }
var images by remember { mutableStateOf<List<ImageBitmap>>(emptyList()) }
var currentWorkflowId by remember { mutableStateOf<String?>(null) }
var currentWorkflowName by remember { mutableStateOf<String?>(null) }
var progress by remember { mutableStateOf<ProgressUpdate?>(null) }
val wsClient = remember { ComfyWsClient(createDefaultHttpClient()) }
val uiState by viewModel.uiState.collectAsState()
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbar) },
topBar = {
TopAppBar(title = { Text(text = when (current.value) {
val current = navController.current.value
TopAppBar(
title = {
Text(
text = when (current) {
Dest.Home -> "工作流"
Dest.Settings -> "设置"
Dest.Import -> "导入工作流"
Dest.Editor -> "编辑工作流"
Dest.Gallery -> "结果"
}) })
Dest.Generate -> "生成"
Dest.Gallery -> "历史"
}
)
},
navigationIcon = {
if (current == Dest.Import || current == Dest.Generate || current == Dest.Gallery) {
IconButton(onClick = { navController.pop() }) {
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
}
}
},
actions = {
if (current == Dest.Generate) {
Icon(
Icons.Filled.History,
contentDescription = null,
modifier = Modifier.clickable { navController.navigate(Dest.Gallery) }
)
}
}
)
},
floatingActionButton = {
if (current.value == Dest.Editor) {
if (navController.current.value == Dest.Generate) {
ExtendedFloatingActionButton(
text = { Text("生成") },
icon = {},
onClick = {
val p = promptState ?: return@ExtendedFloatingActionButton
uiState.prompt.let { p ->
scope.launch {
val repo = AppGraph.comfy
if (uiState.useRandom) {
p.values.find { it.classType == "KSampler" }?.let {
val inputs = it.inputs.toMutableMap()
inputs["seed"] = JsonPrimitive(Random.nextInt(0, Int.MAX_VALUE))
it.inputs = inputs
}
}
snackbar.showSnackbar("提交中…")
val clientId = (currentWorkflowId ?: "client")
scope.launch {
runCatching {
wsClient.listen(
AppGraph.settings.getBaseUrl() ?: "",
clientId,
onUpdate = { progress = it },
onImages = { refs ->
scope.launch {
val imgs = refs.mapNotNull { ref ->
val bytes = repo.image(ref.filename, ref.subfolder, ref.type)
bytes?.let { decodeImage(it) }
}
images = imgs
progress = null
}
}
)
}
}
runCatching { repo.submit(p, clientId) }.onFailure { e ->
snackbar.showSnackbar(e.message ?: "运行失败")
viewModel.submitTask()
}
}
}
@@ -99,60 +92,49 @@ fun AppRoot() {
}
}
) { padding ->
when (current.value) {
when (navController.current.value) {
Dest.Home -> HomeScreen(
contentPadding = padding,
onPick = { item ->
currentWorkflowId = item.id
currentWorkflowName = item.name
promptState = item.prompt
current.value = Dest.Editor
viewModel.setWorkflow(item.id,item.name,item.prompt)
navController.navigate(Dest.Generate)
},
onCreate = { current.value = Dest.Import }
onCreate = { navController.navigate(Dest.Import) }
)
Dest.Settings -> SettingsScreen(padding) {
current.value = Dest.Import
navController.navigate(Dest.Import)
}
Dest.Import -> ImportScreen(
contentPadding = padding,
onImported = {
val name = it.values.firstOrNull()?.meta?.title ?: "工作流"
val id = name + "_" + it.hashCode()
AppGraph.workflows.saveOrUpdate(
WorkflowItem(
id = id,
name = name,
prompt = it
)
)
currentWorkflowId = id
currentWorkflowName = name
promptState = it
current.value = Dest.Editor
val id = it.hashCode().toString() + Clock.System.now().toEpochMilliseconds()
viewModel.setWorkflow(id,name,it)
navController.navigate(Dest.Generate)
},
onError = { msg -> scope.launch { snackbar.showSnackbar(msg) } }
)
Dest.Editor -> EditorScreen(
Dest.Generate -> {
GenerateScreen(
contentPadding = padding,
prompt = promptState ?: emptyMap(),
latestImage = images.firstOrNull(),
progress = progress,
prompt = uiState.prompt,
latestImage = uiState.image,
progress = uiState.progress,
useRandom = uiState.useRandom,
onPromptChange = {
promptState = it
val name = currentWorkflowName ?: it.values.firstOrNull()?.meta?.title ?: "工作流"
val id = currentWorkflowId ?: (name + "_" + it.hashCode()).also { nid -> currentWorkflowId = nid }
currentWorkflowName = name
AppGraph.workflows.saveOrUpdate(
WorkflowItem(
id = id,
name = name,
prompt = it
)
)
viewModel.updatePrompt(it)
},
onRandomSwitchChange = {
viewModel.useRandomSeed(it)
}
)
Dest.Gallery -> GalleryScreen(contentPadding = padding, images = images)
else -> Text("占位页面:" + current.value.name, modifier = Modifier.fillMaxSize())
}
Dest.Gallery -> GalleryScreen(contentPadding = padding,viewModel)
}
}
}
@@ -0,0 +1,134 @@
package moe.uni.comfy.comfyuikmp.ui
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.uni.comfy.comfyuikmp.data.model.ComfyPrompt
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
import moe.uni.comfy.comfyuikmp.data.model.extractImageRefsFromHistoryElement
import moe.uni.comfy.comfyuikmp.data.network.ComfyWsClient
import moe.uni.comfy.comfyuikmp.data.network.ProgressUpdate
import moe.uni.comfy.comfyuikmp.data.network.createDefaultHttpClient
import moe.uni.comfy.comfyuikmp.data.repository.WorkflowItem
import moe.uni.comfy.comfyuikmp.di.AppGraph
import moe.uni.comfy.comfyuikmp.util.decodeImage
class AppViewModel : ViewModel() {
private val _uiState = MutableStateFlow(AppUiState())
val uiState: StateFlow<AppUiState> = _uiState.asStateFlow()
val wsClient = ComfyWsClient(createDefaultHttpClient())
private val wsScope = CoroutineScope(Dispatchers.IO + Job())
private var wsJob: Job? = null
private val _historyImages = MutableStateFlow<List<ImageRef>>(emptyList())
val historyImages: StateFlow<List<ImageRef>> = _historyImages.asStateFlow()
init {
viewModelScope.launch {
var oldId = ""
uiState.collectLatest {
if (it.workflowId != oldId && it.workflowId != "") {
oldId = it.workflowId
startListening()
}
}
}
}
fun startListening() {
wsJob?.cancel()
wsJob = wsScope.launch {
Logger.i{"Listening:${AppGraph.settings.getBaseUrl() + _uiState.value.workflowId}"}
wsClient.listen(AppGraph.settings.getBaseUrl() ?: "", _uiState.value.workflowId, onUpdate = {
_uiState.value = _uiState.value.copy(progress = it)
}, onImages = {
wsScope.launch {
it.map { ref ->
_uiState.value = _uiState.value.copy(image = getImage(ref))
}
}
})
}
}
override fun onCleared() {
super.onCleared()
wsJob?.cancel()
wsClient.close()
}
suspend fun getImage(meta: ImageRef): ImageBitmap? {
AppGraph.comfy.image(meta.filename, meta.subfolder, meta.type)?.let {
return decodeImage(it)
}
return null
}
fun getHistoryImages(size:Int){
viewModelScope.launch {
val map = AppGraph.comfy.history(size) ?: return@launch
val refs = map.values.flatMap { element ->
extractImageRefsFromHistoryElement(element)
}
_historyImages.value = refs.distinctBy { it.filename }.asReversed()
Logger.i { "History images retrieved:${_historyImages.value.size}" }
}
}
fun setWorkflow(id: String,name: String,prompt: ComfyPrompt) {
_uiState.value = _uiState.value.copy(workflowId = id, workflowName = name,prompt = prompt)
viewModelScope.launch {
AppGraph.workflows.saveOrUpdate(
WorkflowItem(
id = id,
name = name,
prompt = prompt
)
)
}
}
fun useRandomSeed(boolean: Boolean){
_uiState.value = _uiState.value.copy(useRandom = boolean)
}
fun updatePrompt(prompt: ComfyPrompt){
_uiState.value = _uiState.value.copy(prompt = prompt)
viewModelScope.launch {
AppGraph.workflows.saveOrUpdate(
WorkflowItem(
id = _uiState.value.workflowId,
name = _uiState.value.workflowName,
prompt = prompt
)
)
}
}
fun submitTask(){
viewModelScope.launch {
AppGraph.comfy.submit(_uiState.value.prompt,_uiState.value.workflowId)
}
}
}
@Stable
data class AppUiState(
val workflowId: String = "",
val workflowName: String = "",
val image: ImageBitmap? = null,
val progress:ProgressUpdate = ProgressUpdate(0f, ""),
val prompt: ComfyPrompt = emptyMap(),
val useRandom: Boolean = false
)
@@ -1,157 +0,0 @@
package moe.uni.comfy.comfyuikmp.ui.editor
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.Button
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.ui.draw.clip
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.intOrNull
import kotlin.random.Random
import moe.uni.comfy.comfyuikmp.data.model.ComfyPrompt
import moe.uni.comfy.comfyuikmp.data.model.updateNodeStringInput
import moe.uni.comfy.comfyuikmp.data.model.updateNodePrimitiveInput
import moe.uni.comfy.comfyuikmp.di.AppGraph
import moe.uni.comfy.comfyuikmp.data.network.ProgressUpdate
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditorScreen(
contentPadding: PaddingValues,
prompt: ComfyPrompt,
latestImage: ImageBitmap?,
progress: ProgressUpdate? = null,
onPromptChange: (ComfyPrompt) -> Unit,
) {
BottomSheetScaffold(
sheetPeekHeight = 56.dp,
sheetContent = {
LazyColumn(modifier = Modifier.padding(16.dp)) {
items(prompt.keys.toList()) { nodeId ->
val node = prompt[nodeId]
val title = node?.meta?.title ?: nodeId
Text(title, style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
node?.inputs?.forEach { (key, value: JsonElement) ->
if (value is JsonPrimitive) {
when {
value.isString -> {
var field by remember(nodeId, key) { mutableStateOf(value.content) }
OutlinedTextField(
value = field,
onValueChange = {
field = it
onPromptChange(updateNodeStringInput(prompt, nodeId, key, it))
},
label = { Text(key) },
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
)
}
value.booleanOrNull != null -> {
var checked by remember(nodeId, key) { mutableStateOf(value.booleanOrNull == true) }
androidx.compose.material3.Switch(
checked = checked,
onCheckedChange = {
checked = it
onPromptChange(updateNodePrimitiveInput(prompt, nodeId, key, JsonPrimitive(it)))
},
)
}
value.intOrNull != null || value.doubleOrNull != null -> {
var text by remember(nodeId, key) { mutableStateOf(value.toString()) }
OutlinedTextField(
value = text,
onValueChange = {
text = it
it.toIntOrNull()?.let { num ->
onPromptChange(updateNodePrimitiveInput(prompt, nodeId, key, JsonPrimitive(num)))
} ?: it.toDoubleOrNull()?.let { d ->
onPromptChange(updateNodePrimitiveInput(prompt, nodeId, key, JsonPrimitive(d)))
}
},
label = { Text(key) },
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
)
if (key.equals("seed", ignoreCase = true)) {
Row(modifier = Modifier.padding(top = 8.dp)) {
Button(onClick = {
val rnd = Random.nextInt(0, Int.MAX_VALUE)
text = rnd.toString()
onPromptChange(updateNodePrimitiveInput(prompt, nodeId, key, JsonPrimitive(rnd)))
}) { Text("随机") }
Spacer(Modifier.width(8.dp))
Text("自动生成随机种子", style = MaterialTheme.typography.bodySmall)
}
}
}
else -> {
OutlinedTextField(
value = value.toString(),
onValueChange = {},
readOnly = true,
label = { Text(key) },
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
)
}
}
} else {
OutlinedTextField(
value = value.toString(),
onValueChange = {},
readOnly = true,
label = { Text(key) },
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
)
}
}
}
}
}
) { innerPadding ->
Column(modifier = Modifier.padding(contentPadding).padding(innerPadding).padding(16.dp)) {
val p = progress
if (p != null) {
LinearProgressIndicator(progress = { p.fraction ?: 0f }, modifier = Modifier.fillMaxWidth())
if (p.message != null) Text(p.message, style = MaterialTheme.typography.bodySmall)
}
if (latestImage != null) {
Image(bitmap = latestImage, contentDescription = null, modifier = Modifier.fillMaxSize().clip(
RoundedCornerShape(15.dp)
))
} else {
Text("暂无生成结果", style = MaterialTheme.typography.bodyMedium)
}
}
}
}
@@ -1,39 +1,68 @@
package moe.uni.comfy.comfyuikmp.ui.gallery
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import moe.uni.comfy.comfyuikmp.di.AppGraph
import moe.uni.comfy.comfyuikmp.ui.AppViewModel
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GalleryScreen(contentPadding: PaddingValues, images: List<ImageBitmap>) {
if (images.isEmpty()) {
fun GalleryScreen(contentPadding: PaddingValues, viewModel: AppViewModel) {
val history by viewModel.historyImages.collectAsState()
// 初次进入时拉取最新历史
LaunchedEffect(Unit) {
viewModel.getHistoryImages(64)
}
if (history.isEmpty()) {
Text("暂无图片", modifier = Modifier.padding(contentPadding).padding(16.dp))
return
}
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 160.dp),
contentPadding = contentPadding
) {
items(images) { img ->
Image(
bitmap = img,
val base = AppGraph.settings.getBaseUrl() ?: ""
LazyColumn(contentPadding = contentPadding, modifier = Modifier.padding(horizontal = 16.dp)) {
items(history, key = { it.filename }) { img ->
val url = buildString {
append(base)
append("/view?filename=")
append(img.filename)
img.subfolder?.let {
append("&subfolder=")
append(it)
}
img.type?.let {
append("&type=")
append(it)
}
}
Card(modifier = Modifier.fillMaxWidth()) {
AsyncImage(
model = url,
contentDescription = null,
modifier = Modifier
.padding(6.dp)
.aspectRatio(1f)
modifier = Modifier.fillMaxWidth(),
)
}
Spacer(Modifier.height(16.dp))
}
}
}
@@ -0,0 +1,202 @@
package moe.uni.comfy.comfyuikmp.ui.generate
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.ui.Alignment
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.intOrNull
import moe.uni.comfy.comfyuikmp.data.model.ComfyPrompt
import moe.uni.comfy.comfyuikmp.data.model.updateNodeStringInput
import moe.uni.comfy.comfyuikmp.data.model.updateNodePrimitiveInput
import moe.uni.comfy.comfyuikmp.data.network.ProgressUpdate
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GenerateScreen(
contentPadding: PaddingValues,
prompt: ComfyPrompt,
latestImage: ImageBitmap?,
progress: ProgressUpdate? = null,
useRandom: Boolean,
onPromptChange: (ComfyPrompt) -> Unit,
onRandomSwitchChange: (Boolean) -> Unit,
) {
val state = rememberBottomSheetScaffoldState()
Scaffold(modifier = Modifier.padding(contentPadding)) {
BottomSheetScaffold(
sheetPeekHeight = 56.dp,
scaffoldState = state,
sheetContent = {
LazyColumn(modifier = Modifier.padding(16.dp)) {
items(prompt.keys.toList()) { nodeId ->
val node = prompt[nodeId]
val title = node?.meta?.title ?: nodeId
Text(
title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 8.dp)
)
node?.inputs?.forEach { (key, value: JsonElement) ->
var inputEnable by remember { mutableStateOf(true) }
if (value is JsonPrimitive) {
when {
value.isString -> {
var field by remember(nodeId, key) { mutableStateOf(value.content) }
OutlinedTextField(
enabled = inputEnable,
value = field,
onValueChange = {
field = it
onPromptChange(updateNodeStringInput(prompt, nodeId, key, it))
},
label = { Text(key) },
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
)
}
value.booleanOrNull != null -> {
var checked by remember(
nodeId,
key
) { mutableStateOf(value.booleanOrNull == true) }
Switch(
enabled = inputEnable,
checked = checked,
onCheckedChange = {
checked = it
onPromptChange(
updateNodePrimitiveInput(
prompt,
nodeId,
key,
JsonPrimitive(it)
)
)
},
)
}
value.intOrNull != null || value.doubleOrNull != null -> {
var text by remember(nodeId, key) { mutableStateOf(value.toString()) }
OutlinedTextField(
enabled = inputEnable,
value = text,
onValueChange = {
text = it
it.toIntOrNull()?.let { num ->
onPromptChange(
updateNodePrimitiveInput(
prompt,
nodeId,
key,
JsonPrimitive(num)
)
)
} ?: it.toDoubleOrNull()?.let { d ->
onPromptChange(
updateNodePrimitiveInput(
prompt,
nodeId,
key,
JsonPrimitive(d)
)
)
}
},
label = { Text(key) },
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
)
//KSampler
if (node.classType == "KSampler" && key.equals("seed", ignoreCase = true)) {
Row(modifier = Modifier.padding(top = 8.dp)) {
Spacer(Modifier.width(8.dp))
Switch(useRandom, onCheckedChange = {
inputEnable = !it
onRandomSwitchChange(it)
})
Text("自动生成随机种子", style = MaterialTheme.typography.bodySmall)
}
}
}
else -> {
OutlinedTextField(
enabled = inputEnable,
value = value.toString(),
onValueChange = {},
readOnly = true,
label = { Text(key) },
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
)
}
}
} else {
OutlinedTextField(
value = value.toString(),
onValueChange = {},
readOnly = true,
label = { Text(key) },
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth()
)
}
}
}
}
},
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
if (progress != null && progress.fraction != 0f && progress.message != "success") {
LinearProgressIndicator(progress = { progress.fraction ?: 0f }, modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp))
if (progress.message != null) Text(progress.message, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(
Alignment.Center))
}
if (latestImage != null) {
Image(
bitmap = latestImage,
contentDescription = null,
modifier = Modifier.fillMaxWidth().padding(top = 32.dp)
)
}
}
}
}
}
@@ -8,6 +8,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -25,12 +28,7 @@ import androidx.compose.material3.Icon
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Brands
import compose.icons.fontawesomeicons.Regular
import compose.icons.fontawesomeicons.brands.FontAwesome
import compose.icons.fontawesomeicons.regular.Edit
import compose.icons.fontawesomeicons.regular.TrashAlt
@Composable
fun HomeScreen(
@@ -58,9 +56,9 @@ fun HomeScreen(
) {
Text(w.name, modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium)
Row(modifier = Modifier.padding(horizontal = 8.dp)) {
IconButton(onClick = { renaming = w; newName = w.name }) { Icon(FontAwesomeIcons.Regular.Edit, contentDescription = null,
IconButton(onClick = { renaming = w; newName = w.name }) { Icon(Icons.Filled.Edit, contentDescription = null,
Modifier.size(24.dp)) }
IconButton(onClick = { AppGraph.workflows.delete(w.id) }) { Icon(FontAwesomeIcons.Regular.TrashAlt, contentDescription = null,
IconButton(onClick = { AppGraph.workflows.delete(w.id) }) { Icon(Icons.Filled.Delete, contentDescription = null,
Modifier.size(24.dp)) }
}
}
@@ -7,3 +7,4 @@ import moe.uni.comfy.comfyuikmp.data.model.ComfyPrompt
expect fun FileImportFab(onImported: (ComfyPrompt) -> Unit, onError: (String) -> Unit)
@@ -6,3 +6,4 @@ import androidx.compose.runtime.Composable
expect fun rememberJsonFilePicker(onPicked: (name: String, content: String) -> Unit): () -> Unit
@@ -1,11 +1,48 @@
package moe.uni.comfy.comfyuikmp.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
enum class Dest {
Home,
Settings,
Import,
Editor,
Generate,
Gallery
}
@Stable
class NavController(startDestination: Dest) {
private val backstack = mutableStateListOf(startDestination)
val current: State<Dest> = derivedStateOf { backstack.last() }
val canPop: Boolean
get() = backstack.size > 1
fun navigate(dest: Dest) {
if (backstack.isEmpty() || backstack.last() != dest) {
backstack.add(dest)
}
}
fun pop(): Boolean {
if (canPop) {
backstack.removeAt(backstack.lastIndex)
return true
}
return false
}
}
@Composable
fun rememberNavController(startDestination: Dest): NavController {
return remember(startDestination) { NavController(startDestination) }
}
@@ -33,3 +33,4 @@ fun SettingsScreen(contentPadding: PaddingValues, onSave: () -> Unit) {
}
@@ -0,0 +1,11 @@
package moe.uni.comfy.comfyuikmp.util
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import moe.uni.comfy.comfyuikmp.ui.AppViewModel
val mainViewModelFactory = viewModelFactory {
initializer {
AppViewModel()
}
}
@@ -16,3 +16,4 @@ actual object KeyValueStore {
}
@@ -17,3 +17,4 @@ actual fun FileImportFab(onImported: (ComfyPrompt) -> Unit, onError: (String) ->
}
@@ -8,3 +8,4 @@ actual fun decodeImage(bytes: ByteArray): ImageBitmap? =
runCatching { Image.makeFromEncoded(bytes).toComposeImageBitmap() }.getOrNull()
+3
View File
@@ -14,6 +14,8 @@ junit = "4.13.2"
kotlin = "2.2.20"
ktor = "3.0.0"
serialization = "1.7.3"
androidx-viewmodel = "2.9.4"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@@ -34,6 +36,7 @@ ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "kto
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-viewmodel" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
+100 -96
View File
@@ -167,6 +167,94 @@
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
1421DB7F95CDAE57944E10C9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = NLHG3FFX4L;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Comfy;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = moe.uni.comfy.comfyuikmp;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
35E05C7FC47D3052DBB14A86 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = CF9414654D7F1C0CBABF3105 /* Configuration */;
baseConfigurationReferenceRelativePath = Config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
B418F86C9121F76AC29E0C68 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = CF9414654D7F1C0CBABF3105 /* Configuration */;
@@ -232,92 +320,6 @@
};
name = Debug;
};
35E05C7FC47D3052DBB14A86 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = CF9414654D7F1C0CBABF3105 /* Configuration */;
baseConfigurationReferenceRelativePath = Config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
1421DB7F95CDAE57944E10C9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
CF145A26F4A0096FF05CC5ED /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -327,10 +329,11 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "${TEAM_ID}";
DEVELOPMENT_TEAM = NLHG3FFX4L;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Comfy;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -340,6 +343,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = moe.uni.comfy.comfyuikmp;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -349,15 +353,6 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
CE072D131AB21E31A22C0C65 /* Build configuration list for PBXProject "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B418F86C9121F76AC29E0C68 /* Debug */,
35E05C7FC47D3052DBB14A86 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C5F0489CAF21F1B9DDCDE9EC /* Build configuration list for PBXNativeTarget "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -367,6 +362,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
CE072D131AB21E31A22C0C65 /* Build configuration list for PBXProject "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B418F86C9121F76AC29E0C68 /* Debug */,
35E05C7FC47D3052DBB14A86 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = D3A0FA2F57232E5614C0AC86 /* Project object */;