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(compose.preview)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.ktor.client.okhttp) implementation(libs.ktor.client.okhttp)
implementation("io.ktor:ktor-client-android:3.3.0")
} }
commonMain.dependencies { commonMain.dependencies {
implementation(compose.runtime) implementation(compose.runtime)
@@ -52,12 +51,12 @@ kotlin {
implementation(compose.materialIconsExtended) implementation(compose.materialIconsExtended)
implementation("co.touchlab:kermit:2.0.4") //Add latest version 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-compose:3.3.0")
implementation("io.coil-kt.coil3:coil-network-ktor3:3.3.0")
} }
commonTest.dependencies { commonTest.dependencies {
implementation(libs.kotlin.test) implementation(libs.kotlin.test)
} }
iosMain.dependencies { iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:3.3.0")
implementation(libs.ktor.client.darwin) implementation(libs.ktor.client.darwin)
} }
} }
@@ -1,6 +1,7 @@
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.padding
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
@@ -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.Dest
import moe.uni.comfy.comfyuikmp.ui.navigation.rememberNavController 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.util.mainViewModelFactory import moe.uni.comfy.comfyuikmp.util.mainViewModelFactory
import kotlin.random.Random import kotlin.random.Random
import kotlin.time.Clock import kotlin.time.Clock
@@ -33,6 +35,13 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
val snackbar = remember { SnackbarHostState() } val snackbar = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val uiState by viewModel.uiState.collectAsState() 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( Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbar) }, snackbarHost = { SnackbarHost(hostState = snackbar) },
@@ -47,12 +56,16 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
Dest.Import -> "导入工作流" Dest.Import -> "导入工作流"
Dest.Generate -> "生成" Dest.Generate -> "生成"
Dest.Gallery -> "历史" Dest.Gallery -> "历史"
Dest.View -> "查看"
} }
) )
}, },
navigationIcon = { navigationIcon = {
if (current == Dest.Import || current == Dest.Generate || current == Dest.Gallery) { if (current == Dest.Import || current == Dest.Generate || current == Dest.Gallery || current == Dest.View) {
IconButton(onClick = { navController.pop() }) { IconButton(onClick = {
if (current == Dest.View) viewModel.clearOpenImage()
navController.pop()
}) {
Icon(Icons.Filled.ArrowBack, contentDescription = "返回") Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
} }
} }
@@ -112,7 +125,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
val name = it.values.firstOrNull()?.meta?.title ?: "工作流" val name = it.values.firstOrNull()?.meta?.title ?: "工作流"
val id = it.hashCode().toString() + Clock.System.now().toEpochMilliseconds() val id = it.hashCode().toString() + Clock.System.now().toEpochMilliseconds()
viewModel.setWorkflow(id,name,it) viewModel.setWorkflow(id,name,it)
navController.navigate(Dest.Generate) navController.pop()
}, },
onError = { msg -> scope.launch { snackbar.showSnackbar(msg) } } onError = { msg -> scope.launch { snackbar.showSnackbar(msg) } }
) )
@@ -124,17 +137,25 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
latestImage = uiState.image, latestImage = uiState.image,
progress = uiState.progress, progress = uiState.progress,
useRandom = uiState.useRandom, useRandom = uiState.useRandom,
latestImageRef = viewModel.latestImageRef.collectAsState().value,
onPromptChange = { onPromptChange = {
viewModel.updatePrompt(it) viewModel.updatePrompt(it)
}, },
onRandomSwitchChange = { onRandomSwitchChange = {
viewModel.useRandomSeed(it) viewModel.useRandomSeed(it)
} },
onOpenImage = { ref -> viewModel.openImage(ref) }
) )
} }
Dest.Gallery -> GalleryScreen(contentPadding = padding,viewModel) 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()) private val _historyImages = MutableStateFlow<List<ImageRef>>(emptyList())
val historyImages: StateFlow<List<ImageRef>> = _historyImages.asStateFlow() 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 { init {
viewModelScope.launch { viewModelScope.launch {
var oldId = "" var oldId = ""
@@ -54,6 +60,7 @@ class AppViewModel : ViewModel() {
}, onImages = { }, onImages = {
wsScope.launch { wsScope.launch {
it.map { ref -> it.map { ref ->
_latestImageRef.value = ref
_uiState.value = _uiState.value.copy(image = getImage(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 @Stable
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.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
@@ -16,10 +17,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage 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
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @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( AsyncImage(
model = url, model = url,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
} }
} }
} }
@@ -25,6 +25,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch 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 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
@@ -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.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
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -43,8 +45,10 @@ fun GenerateScreen(
latestImage: ImageBitmap?, latestImage: ImageBitmap?,
progress: ProgressUpdate? = null, progress: ProgressUpdate? = null,
useRandom: Boolean, useRandom: Boolean,
latestImageRef: ImageRef? = null,
onPromptChange: (ComfyPrompt) -> Unit, onPromptChange: (ComfyPrompt) -> Unit,
onRandomSwitchChange: (Boolean) -> Unit, onRandomSwitchChange: (Boolean) -> Unit,
onOpenImage: (ImageRef) -> Unit = {},
) { ) {
val state = rememberBottomSheetScaffoldState() val state = rememberBottomSheetScaffoldState()
Scaffold(modifier = Modifier.padding(contentPadding)) { Scaffold(modifier = Modifier.padding(contentPadding)) {
@@ -186,11 +190,22 @@ fun GenerateScreen(
} }
if (latestImage != null) { if (latestImage != null) {
Image( if (latestImageRef != null) {
bitmap = latestImage, Image(
contentDescription = null, bitmap = latestImage,
modifier = Modifier.fillMaxWidth().padding(top = 32.dp) 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 onCreate: () -> Unit
) { ) {
val repo = remember { AppGraph.workflows } val repo = remember { AppGraph.workflows }
val items = repo.list() var items by remember { mutableStateOf(repo.list()) }
var renaming by remember { mutableStateOf<WorkflowItem?>(null) } var renaming by remember { mutableStateOf<WorkflowItem?>(null) }
var newName by remember { mutableStateOf("") } var newName by remember { mutableStateOf("") }
LazyColumn(contentPadding = contentPadding) { LazyColumn(contentPadding = contentPadding) {
@@ -58,7 +58,7 @@ fun HomeScreen(
Row(modifier = Modifier.padding(horizontal = 8.dp)) { Row(modifier = Modifier.padding(horizontal = 8.dp)) {
IconButton(onClick = { renaming = w; newName = w.name }) { Icon(Icons.Filled.Edit, contentDescription = null, IconButton(onClick = { renaming = w; newName = w.name }) { Icon(Icons.Filled.Edit, contentDescription = null,
Modifier.size(24.dp)) } 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)) } Modifier.size(24.dp)) }
} }
} }
@@ -70,6 +70,7 @@ fun HomeScreen(
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(r.id, newName.ifBlank { r.name })
items = repo.list()
renaming = null renaming = null
}) { Text("确定") } }) { Text("确定") }
}, dismissButton = { }, dismissButton = {
@@ -13,7 +13,8 @@ enum class Dest {
Settings, Settings,
Import, Import,
Generate, Generate,
Gallery Gallery,
View
} }
@Stable @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
}
}
)
}
}