fix: home screen & image load
This commit is contained in:
@@ -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
|
||||
|
||||
+7
-2
@@ -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))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-5
@@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user