fix: home screen & image load

This commit is contained in:
AnranYus
2025-10-06 20:03:00 +08:00
parent 342a1a370d
commit 289aef4d84
8 changed files with 151 additions and 16 deletions
+1 -2
View File
@@ -31,7 +31,6 @@ 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)
@@ -52,12 +51,12 @@ kotlin {
implementation(compose.materialIconsExtended)
implementation("co.touchlab:kermit:2.0.4") //Add latest version
implementation("io.coil-kt.coil3:coil-compose:3.3.0")
implementation("io.coil-kt.coil3:coil-network-ktor3: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)
}
}
@@ -1,6 +1,7 @@
package moe.uni.comfy.comfyuikmp.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.History
@@ -18,6 +19,7 @@ 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.ui.view.ViewScreen
import moe.uni.comfy.comfyuikmp.util.mainViewModelFactory
import kotlin.random.Random
import kotlin.time.Clock
@@ -33,6 +35,13 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
val snackbar = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val uiState by viewModel.uiState.collectAsState()
val viewTarget by viewModel.viewTarget.collectAsState()
LaunchedEffect(viewTarget) {
if (viewTarget != null && navController.current.value != Dest.View) {
navController.navigate(Dest.View)
}
}
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbar) },
@@ -47,12 +56,16 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
Dest.Import -> "导入工作流"
Dest.Generate -> "生成"
Dest.Gallery -> "历史"
Dest.View -> "查看"
}
)
},
navigationIcon = {
if (current == Dest.Import || current == Dest.Generate || current == Dest.Gallery) {
IconButton(onClick = { navController.pop() }) {
if (current == Dest.Import || current == Dest.Generate || current == Dest.Gallery || current == Dest.View) {
IconButton(onClick = {
if (current == Dest.View) viewModel.clearOpenImage()
navController.pop()
}) {
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
}
}
@@ -112,7 +125,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
val name = it.values.firstOrNull()?.meta?.title ?: "工作流"
val id = it.hashCode().toString() + Clock.System.now().toEpochMilliseconds()
viewModel.setWorkflow(id,name,it)
navController.navigate(Dest.Generate)
navController.pop()
},
onError = { msg -> scope.launch { snackbar.showSnackbar(msg) } }
)
@@ -124,17 +137,25 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
latestImage = uiState.image,
progress = uiState.progress,
useRandom = uiState.useRandom,
latestImageRef = viewModel.latestImageRef.collectAsState().value,
onPromptChange = {
viewModel.updatePrompt(it)
},
onRandomSwitchChange = {
viewModel.useRandomSeed(it)
}
},
onOpenImage = { ref -> viewModel.openImage(ref) }
)
}
Dest.Gallery -> GalleryScreen(contentPadding = padding,viewModel)
Dest.View -> {
val ref by viewModel.viewTarget.collectAsState()
if (ref != null) ViewScreen(ref!!, contentPadding = padding)
else Text("无可查看图片", modifier = Modifier.padding(padding))
}
}
}
}
@@ -33,6 +33,12 @@ class AppViewModel : ViewModel() {
private val _historyImages = MutableStateFlow<List<ImageRef>>(emptyList())
val historyImages: StateFlow<List<ImageRef>> = _historyImages.asStateFlow()
private val _latestImageRef = MutableStateFlow<ImageRef?>(null)
val latestImageRef: StateFlow<ImageRef?> = _latestImageRef.asStateFlow()
private val _viewTarget = MutableStateFlow<ImageRef?>(null)
val viewTarget: StateFlow<ImageRef?> = _viewTarget.asStateFlow()
init {
viewModelScope.launch {
var oldId = ""
@@ -54,6 +60,7 @@ class AppViewModel : ViewModel() {
}, onImages = {
wsScope.launch {
it.map { ref ->
_latestImageRef.value = ref
_uiState.value = _uiState.value.copy(image = getImage(ref))
}
}
@@ -121,6 +128,14 @@ class AppViewModel : ViewModel() {
}
}
fun openImage(ref: ImageRef) {
_viewTarget.value = ref
}
fun clearOpenImage() {
_viewTarget.value = null
}
}
@Stable
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.clickable
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -16,10 +17,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import moe.uni.comfy.comfyuikmp.di.AppGraph
import moe.uni.comfy.comfyuikmp.ui.AppViewModel
import moe.uni.comfy.comfyuikmp.ui.navigation.Dest
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -53,15 +56,17 @@ fun GalleryScreen(contentPadding: PaddingValues, viewModel: AppViewModel) {
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Card(modifier = Modifier.fillMaxWidth().aspectRatio(3f / 4f).clickable {
viewModel.openImage(img)
}) {
AsyncImage(
model = url,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth(),
)
}
Spacer(Modifier.height(16.dp))
}
}
}
@@ -25,6 +25,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.ui.Alignment
import androidx.compose.foundation.clickable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
@@ -34,6 +35,7 @@ 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
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -43,8 +45,10 @@ fun GenerateScreen(
latestImage: ImageBitmap?,
progress: ProgressUpdate? = null,
useRandom: Boolean,
latestImageRef: ImageRef? = null,
onPromptChange: (ComfyPrompt) -> Unit,
onRandomSwitchChange: (Boolean) -> Unit,
onOpenImage: (ImageRef) -> Unit = {},
) {
val state = rememberBottomSheetScaffoldState()
Scaffold(modifier = Modifier.padding(contentPadding)) {
@@ -186,11 +190,22 @@ fun GenerateScreen(
}
if (latestImage != null) {
Image(
bitmap = latestImage,
contentDescription = null,
modifier = Modifier.fillMaxWidth().padding(top = 32.dp)
)
if (latestImageRef != null) {
Image(
bitmap = latestImage,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.padding(top = 32.dp)
.clickable { onOpenImage(latestImageRef) }
)
} else {
Image(
bitmap = latestImage,
contentDescription = null,
modifier = Modifier.fillMaxWidth().padding(top = 32.dp)
)
}
}
}
}
@@ -37,7 +37,7 @@ fun HomeScreen(
onCreate: () -> Unit
) {
val repo = remember { AppGraph.workflows }
val items = repo.list()
var items by remember { mutableStateOf(repo.list()) }
var renaming by remember { mutableStateOf<WorkflowItem?>(null) }
var newName by remember { mutableStateOf("") }
LazyColumn(contentPadding = contentPadding) {
@@ -58,7 +58,7 @@ fun HomeScreen(
Row(modifier = Modifier.padding(horizontal = 8.dp)) {
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(Icons.Filled.Delete, contentDescription = null,
IconButton(onClick = { AppGraph.workflows.delete(w.id); items = repo.list() }) { Icon(Icons.Filled.Delete, contentDescription = null,
Modifier.size(24.dp)) }
}
}
@@ -70,6 +70,7 @@ fun HomeScreen(
AlertDialog(onDismissRequest = { renaming = null }, confirmButton = {
Button(onClick = {
AppGraph.workflows.rename(r.id, newName.ifBlank { r.name })
items = repo.list()
renaming = null
}) { Text("确定") }
}, dismissButton = {
@@ -13,7 +13,8 @@ enum class Dest {
Settings,
Import,
Generate,
Gallery
Gallery,
View
}
@Stable
@@ -0,0 +1,78 @@
package moe.uni.comfy.comfyuikmp.ui.view
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
import moe.uni.comfy.comfyuikmp.di.AppGraph
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.geometry.Offset
@Composable
fun ViewScreen(ref: ImageRef, contentPadding: PaddingValues) {
val base = AppGraph.settings.getBaseUrl() ?: ""
val url = remember(ref, base) {
buildString {
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 rotationState = remember { mutableFloatStateOf(0f) }
val translationState = remember { mutableStateOf(Offset.Zero) }
Box(Modifier.fillMaxSize().padding(contentPadding)) {
AsyncImage(
model = url,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(
scaleX = scaleState.floatValue,
scaleY = scaleState.floatValue,
rotationZ = rotationState.floatValue,
translationX = translationState.value.x,
translationY = translationState.value.y
)
.pointerInput("doubleTapReset") {
detectTapGestures(
onDoubleTap = {
scaleState.floatValue = 1f
rotationState.floatValue = 0f
translationState.value = Offset.Zero
}
)
}
.pointerInput("transform") {
detectTransformGestures { _, pan, zoom, rotation ->
val newScale = (scaleState.floatValue * zoom).coerceIn(0.5f, 8f)
scaleState.floatValue = newScale
translationState.value += pan
}
}
)
}
}