fix: bugs
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:name=".ComfyApp"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package moe.uni.comfy.comfyuikmp
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
class ComfyApp: Application() {
|
||||||
|
companion object{
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
lateinit var context: Context
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
context = applicationContext
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package moe.uni.comfy.comfyuikmp.util
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||||
|
import moe.uni.comfy.comfyuikmp.ComfyApp
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
actual suspend fun saveImageToLocal(image: ImageBitmap, name: String): Result<Boolean> {
|
||||||
|
val bitmap = image.asAndroidBitmap()
|
||||||
|
|
||||||
|
val imagesCollection =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||||
|
} else {
|
||||||
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.Images.Media.DISPLAY_NAME, "$name.png")
|
||||||
|
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/ComfyApp")
|
||||||
|
put(MediaStore.Images.Media.IS_PENDING, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val resolver = ComfyApp.context.contentResolver
|
||||||
|
val uri = resolver.insert(imagesCollection, contentValues)
|
||||||
|
uri?.let {
|
||||||
|
try {
|
||||||
|
resolver.openOutputStream(uri)?.use { out ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||||
|
} ?: throw kotlinx.io.IOException("OpenOutputStream returned null")
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
contentValues.clear()
|
||||||
|
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||||
|
resolver.update(uri, contentValues, null, null)
|
||||||
|
}
|
||||||
|
return Result.success(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 出错时删除占位条目,避免相册里出现空文件
|
||||||
|
runCatching { resolver.delete(uri, null, null) }
|
||||||
|
e.printStackTrace()
|
||||||
|
return Result.failure(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return Result.failure(kotlinx.io.IOException("Failed to create new MediaStore record."))
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,6 +4,11 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
|
||||||
|
|
||||||
|
object NodeType{
|
||||||
|
const val KSampler = "KSampler"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用 ComfyUI 节点模型,尽量保持结构宽松以兼容不同节点类型与输入形态。
|
* 通用 ComfyUI 节点模型,尽量保持结构宽松以兼容不同节点类型与输入形态。
|
||||||
*/
|
*/
|
||||||
|
|||||||
+3
-1
@@ -6,12 +6,14 @@ import kotlinx.serialization.encodeToString
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import moe.uni.comfy.comfyuikmp.data.model.ComfyPrompt
|
import moe.uni.comfy.comfyuikmp.data.model.ComfyPrompt
|
||||||
import moe.uni.comfy.comfyuikmp.data.settings.KeyValueStore
|
import moe.uni.comfy.comfyuikmp.data.settings.KeyValueStore
|
||||||
|
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WorkflowItem(
|
data class WorkflowItem(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val prompt: ComfyPrompt
|
val prompt: ComfyPrompt,
|
||||||
|
val cover: ImageRef? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
class WorkflowRepository {
|
class WorkflowRepository {
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
package moe.uni.comfy.comfyuikmp.ui
|
package moe.uni.comfy.comfyuikmp.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.History
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import moe.uni.comfy.comfyuikmp.data.model.NodeType
|
||||||
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
||||||
import moe.uni.comfy.comfyuikmp.ui.gallery.GalleryScreen
|
import moe.uni.comfy.comfyuikmp.ui.gallery.GalleryScreen
|
||||||
import moe.uni.comfy.comfyuikmp.ui.generate.GenerateScreen
|
import moe.uni.comfy.comfyuikmp.ui.generate.GenerateScreen
|
||||||
@@ -21,6 +26,7 @@ import moe.uni.comfy.comfyuikmp.ui.navigation.rememberNavController
|
|||||||
import moe.uni.comfy.comfyuikmp.ui.settings.SettingsScreen
|
import moe.uni.comfy.comfyuikmp.ui.settings.SettingsScreen
|
||||||
import moe.uni.comfy.comfyuikmp.ui.view.ViewScreen
|
import moe.uni.comfy.comfyuikmp.ui.view.ViewScreen
|
||||||
import moe.uni.comfy.comfyuikmp.util.mainViewModelFactory
|
import moe.uni.comfy.comfyuikmp.util.mainViewModelFactory
|
||||||
|
import moe.uni.comfy.comfyuikmp.util.saveImageToLocal
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
@@ -31,6 +37,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
|
|||||||
val hasSaved = remember { AppGraph.workflows.list().isNotEmpty() }
|
val hasSaved = remember { AppGraph.workflows.list().isNotEmpty() }
|
||||||
val startDest = if (hasSaved) Dest.Home else Dest.Settings
|
val startDest = if (hasSaved) Dest.Home else Dest.Settings
|
||||||
val navController = rememberNavController(startDest)
|
val navController = rememberNavController(startDest)
|
||||||
|
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
val snackbar = remember { SnackbarHostState() }
|
val snackbar = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -72,11 +79,29 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (current == Dest.Generate) {
|
if (current == Dest.Generate) {
|
||||||
|
if (uiState.image!=null){
|
||||||
|
Icon(Icons.Filled.Save, contentDescription = null, modifier = Modifier.clickable{
|
||||||
|
uiState.image?.let {
|
||||||
|
scope.launch {
|
||||||
|
val result = saveImageToLocal(it,"${uiState.workflowName}_${Random.nextInt()}.png")
|
||||||
|
|
||||||
|
if (result.isSuccess){
|
||||||
|
snackbar.showSnackbar("保存成功")
|
||||||
|
}else{
|
||||||
|
snackbar.showSnackbar(result.exceptionOrNull()?.message?:"保存失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Spacer(Modifier.size(16.dp))
|
||||||
|
}
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.History,
|
Icons.Filled.History,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.clickable { navController.navigate(Dest.Gallery) }
|
modifier = Modifier.clickable { navController.navigate(Dest.Gallery) }
|
||||||
)
|
)
|
||||||
|
Spacer(Modifier.size(16.dp))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -90,7 +115,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
|
|||||||
uiState.prompt.let { p ->
|
uiState.prompt.let { p ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (uiState.useRandom) {
|
if (uiState.useRandom) {
|
||||||
p.values.find { it.classType == "KSampler" }?.let {
|
p.values.find { it.classType == NodeType.KSampler }?.let {
|
||||||
val inputs = it.inputs.toMutableMap()
|
val inputs = it.inputs.toMutableMap()
|
||||||
inputs["seed"] = JsonPrimitive(Random.nextInt(0, Int.MAX_VALUE))
|
inputs["seed"] = JsonPrimitive(Random.nextInt(0, Int.MAX_VALUE))
|
||||||
it.inputs = inputs
|
it.inputs = inputs
|
||||||
@@ -145,7 +170,8 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
|
|||||||
viewModel.useRandomSeed(it)
|
viewModel.useRandomSeed(it)
|
||||||
|
|
||||||
},
|
},
|
||||||
onOpenImage = { ref -> viewModel.openImage(ref) }
|
onOpenImage = { ref -> viewModel.openImage(ref) },
|
||||||
|
onSetCover = { }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import moe.uni.comfy.comfyuikmp.data.network.createDefaultHttpClient
|
|||||||
import moe.uni.comfy.comfyuikmp.data.repository.WorkflowItem
|
import moe.uni.comfy.comfyuikmp.data.repository.WorkflowItem
|
||||||
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
||||||
import moe.uni.comfy.comfyuikmp.util.decodeImage
|
import moe.uni.comfy.comfyuikmp.util.decodeImage
|
||||||
|
import moe.uni.comfy.comfyuikmp.util.saveImageToLocal
|
||||||
|
|
||||||
class AppViewModel : ViewModel() {
|
class AppViewModel : ViewModel() {
|
||||||
private val _uiState = MutableStateFlow(AppUiState())
|
private val _uiState = MutableStateFlow(AppUiState())
|
||||||
@@ -145,5 +146,5 @@ data class AppUiState(
|
|||||||
val image: ImageBitmap? = null,
|
val image: ImageBitmap? = null,
|
||||||
val progress:ProgressUpdate = ProgressUpdate(0f, ""),
|
val progress:ProgressUpdate = ProgressUpdate(0f, ""),
|
||||||
val prompt: ComfyPrompt = emptyMap(),
|
val prompt: ComfyPrompt = emptyMap(),
|
||||||
val useRandom: Boolean = false
|
val useRandom: Boolean = true
|
||||||
)
|
)
|
||||||
+12
-16
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -23,6 +24,12 @@ import coil3.compose.AsyncImage
|
|||||||
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
||||||
import moe.uni.comfy.comfyuikmp.ui.AppViewModel
|
import moe.uni.comfy.comfyuikmp.ui.AppViewModel
|
||||||
import moe.uni.comfy.comfyuikmp.ui.navigation.Dest
|
import moe.uni.comfy.comfyuikmp.ui.navigation.Dest
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import moe.uni.comfy.comfyuikmp.util.buildImageUrl
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -42,23 +49,12 @@ fun GalleryScreen(contentPadding: PaddingValues, viewModel: AppViewModel) {
|
|||||||
|
|
||||||
LazyColumn(contentPadding = contentPadding, modifier = Modifier.padding(horizontal = 16.dp)) {
|
LazyColumn(contentPadding = contentPadding, modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
items(history, key = { it.filename }) { img ->
|
items(history, key = { it.filename }) { img ->
|
||||||
val url = buildString {
|
val url = buildImageUrl(img)
|
||||||
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().aspectRatio(3f / 4f).clickable {
|
|
||||||
viewModel.openImage(img)
|
Card(modifier = Modifier.fillMaxWidth().aspectRatio(3f / 4f).combinedClickable(
|
||||||
}) {
|
onClick = { viewModel.openImage(img) }
|
||||||
|
)) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = url,
|
model = url,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|||||||
+27
-2
@@ -26,6 +26,7 @@ import androidx.compose.material3.Switch
|
|||||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import kotlinx.serialization.json.booleanOrNull
|
import kotlinx.serialization.json.booleanOrNull
|
||||||
@@ -36,6 +37,9 @@ import moe.uni.comfy.comfyuikmp.data.model.updateNodeStringInput
|
|||||||
import moe.uni.comfy.comfyuikmp.data.model.updateNodePrimitiveInput
|
import moe.uni.comfy.comfyuikmp.data.model.updateNodePrimitiveInput
|
||||||
import moe.uni.comfy.comfyuikmp.data.network.ProgressUpdate
|
import moe.uni.comfy.comfyuikmp.data.network.ProgressUpdate
|
||||||
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
|
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import moe.uni.comfy.comfyuikmp.data.model.NodeType
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -49,6 +53,7 @@ fun GenerateScreen(
|
|||||||
onPromptChange: (ComfyPrompt) -> Unit,
|
onPromptChange: (ComfyPrompt) -> Unit,
|
||||||
onRandomSwitchChange: (Boolean) -> Unit,
|
onRandomSwitchChange: (Boolean) -> Unit,
|
||||||
onOpenImage: (ImageRef) -> Unit = {},
|
onOpenImage: (ImageRef) -> Unit = {},
|
||||||
|
onSetCover: (ImageRef) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val state = rememberBottomSheetScaffoldState()
|
val state = rememberBottomSheetScaffoldState()
|
||||||
Scaffold(modifier = Modifier.padding(contentPadding)) {
|
Scaffold(modifier = Modifier.padding(contentPadding)) {
|
||||||
@@ -141,7 +146,8 @@ fun GenerateScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
//KSampler
|
//KSampler
|
||||||
if (node.classType == "KSampler" && key.equals("seed", ignoreCase = true)) {
|
if (node.classType == NodeType.KSampler && key.equals("seed", ignoreCase = true)) {
|
||||||
|
inputEnable = !useRandom
|
||||||
Row(modifier = Modifier.padding(top = 8.dp)) {
|
Row(modifier = Modifier.padding(top = 8.dp)) {
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Switch(useRandom, onCheckedChange = {
|
Switch(useRandom, onCheckedChange = {
|
||||||
@@ -189,6 +195,7 @@ fun GenerateScreen(
|
|||||||
Alignment.Center))
|
Alignment.Center))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showSetCover by remember { mutableStateOf(false) }
|
||||||
if (latestImage != null) {
|
if (latestImage != null) {
|
||||||
if (latestImageRef != null) {
|
if (latestImageRef != null) {
|
||||||
Image(
|
Image(
|
||||||
@@ -197,7 +204,10 @@ fun GenerateScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 32.dp)
|
.padding(top = 32.dp)
|
||||||
.clickable { onOpenImage(latestImageRef) }
|
.combinedClickable(
|
||||||
|
onClick = { onOpenImage(latestImageRef) },
|
||||||
|
onLongClick = { showSetCover = true }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Image(
|
Image(
|
||||||
@@ -207,6 +217,21 @@ fun GenerateScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showSetCover && latestImageRef != null) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showSetCover = false },
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = {
|
||||||
|
onSetCover(latestImageRef)
|
||||||
|
showSetCover = false
|
||||||
|
}) { Text("设为封面") }
|
||||||
|
},
|
||||||
|
dismissButton = { Button(onClick = { showSetCover = false }) { Text("取消") } },
|
||||||
|
title = { Text("设置封面") },
|
||||||
|
text = { Text("将当前图片设为该工作流的封面?") }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -28,6 +32,10 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import moe.uni.comfy.comfyuikmp.util.buildImageUrl
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -49,27 +57,48 @@ fun HomeScreen(
|
|||||||
) { Text("新建/导入工作流", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium) }
|
) { Text("新建/导入工作流", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium) }
|
||||||
}
|
}
|
||||||
items(items) { w ->
|
items(items) { w ->
|
||||||
Card(modifier = Modifier
|
Card(
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
.clickable { onPick(w) }
|
.fillMaxWidth()
|
||||||
|
.clickable { onPick(w) },
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||||
) {
|
) {
|
||||||
Text(w.name, modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium)
|
if (w.cover != null) {
|
||||||
Row(modifier = Modifier.padding(horizontal = 8.dp)) {
|
AsyncImage(
|
||||||
IconButton(onClick = { renaming = w; newName = w.name }) { Icon(Icons.Filled.Edit, contentDescription = null,
|
model = buildImageUrl(w.cover),
|
||||||
Modifier.size(24.dp)) }
|
contentDescription = null,
|
||||||
IconButton(onClick = { AppGraph.workflows.delete(w.id); items = repo.list() }) { Icon(Icons.Filled.Delete, contentDescription = null,
|
contentScale = ContentScale.Crop,
|
||||||
Modifier.size(24.dp)) }
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(4f / 3f)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 占位封面区域
|
||||||
|
androidx.compose.foundation.layout.Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(4f / 3f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Text(w.name, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Row(modifier = Modifier.padding(top = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = { renaming = w; newName = w.name }) { Icon(Icons.Filled.Edit, contentDescription = null,
|
||||||
|
Modifier.size(24.dp)) }
|
||||||
|
IconButton(onClick = { AppGraph.workflows.delete(w.id); items = repo.list() }) { Icon(Icons.Filled.Delete, contentDescription = null,
|
||||||
|
Modifier.size(24.dp)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val r = renaming
|
if (renaming != null) {
|
||||||
if (r != null) {
|
|
||||||
AlertDialog(onDismissRequest = { renaming = null }, confirmButton = {
|
AlertDialog(onDismissRequest = { renaming = null }, confirmButton = {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
AppGraph.workflows.rename(r.id, newName.ifBlank { r.name })
|
AppGraph.workflows.rename(renaming!!.id, newName.ifBlank { renaming!!.name })
|
||||||
items = repo.list()
|
items = repo.list()
|
||||||
renaming = null
|
renaming = null
|
||||||
}) { Text("确定") }
|
}) { Text("确定") }
|
||||||
|
|||||||
@@ -19,24 +19,13 @@ import moe.uni.comfy.comfyuikmp.data.model.ImageRef
|
|||||||
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import moe.uni.comfy.comfyuikmp.util.buildImageUrl
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ViewScreen(ref: ImageRef, contentPadding: PaddingValues) {
|
fun ViewScreen(ref: ImageRef, contentPadding: PaddingValues) {
|
||||||
val base = AppGraph.settings.getBaseUrl() ?: ""
|
val base = AppGraph.settings.getBaseUrl() ?: ""
|
||||||
val url = remember(ref, base) {
|
val url = remember(ref, base) {
|
||||||
buildString {
|
buildImageUrl(ref)
|
||||||
append(base)
|
|
||||||
append("/view?filename=")
|
|
||||||
append(ref.filename)
|
|
||||||
ref.subfolder?.let {
|
|
||||||
append("&subfolder=")
|
|
||||||
append(it)
|
|
||||||
}
|
|
||||||
ref.type?.let {
|
|
||||||
append("&type=")
|
|
||||||
append(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val scaleState = remember { mutableFloatStateOf(1f) }
|
val scaleState = remember { mutableFloatStateOf(1f) }
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package moe.uni.comfy.comfyuikmp.util
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
|
||||||
|
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
||||||
|
|
||||||
|
fun buildImageUrl(ref: ImageRef): String {
|
||||||
|
val base = AppGraph.settings.getBaseUrl() ?: ""
|
||||||
|
return buildString {
|
||||||
|
append(base)
|
||||||
|
append("/view?filename=")
|
||||||
|
append(ref.filename)
|
||||||
|
ref.subfolder?.let {
|
||||||
|
append("&subfolder=")
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
ref.type?.let {
|
||||||
|
append("&type=")
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect suspend fun saveImageToLocal(image: ImageBitmap, name: String): Result<Boolean>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -19,11 +19,6 @@ actual fun createPlatformHttpClient(configure: HttpClientConfig<*>.() -> Unit):
|
|||||||
install(UserAgent) {
|
install(UserAgent) {
|
||||||
agent = "ComfyUIKMP-iOS"
|
agent = "ComfyUIKMP-iOS"
|
||||||
}
|
}
|
||||||
defaultRequest {
|
|
||||||
url {
|
|
||||||
protocol = URLProtocol.HTTP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
configure()
|
configure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package moe.uni.comfy.comfyuikmp.util
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import coil3.Bitmap
|
||||||
|
|
||||||
|
actual suspend fun saveImageToLocal(image: ImageBitmap, name: String): Result<Boolean> {
|
||||||
|
return Result.failure(kotlinx.io.IOException("Failed to create new MediaStore record."))
|
||||||
|
}
|
||||||
@@ -4,12 +4,47 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>需要访问 ComfyUI 服务以生成图像</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||||
|
<true/>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSExceptionDomains</key>
|
||||||
|
<dict>
|
||||||
|
<!-- 示例:开发期允许局域网 IP 和自签名证书。可按需添加具体域名/IP。 -->
|
||||||
|
<key>localhost</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>127.0.0.1</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>10.0.0.0/8</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>172.16.0.0/12</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>192.168.0.0/16</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
Reference in New Issue
Block a user