mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
App mode - more updates & fixes (#9137)
## Summary - fix sizing of sidebars in app mode - update feedback button to match design - update job queue notification - clickable queue spinner item to allow clear queue - refactor mode out of store to specific workflow instance - support different saved vs active mode - other styling/layout tweaks ## Changes - **What**: Changes the store to a composable and moves the mode state to the workflow. - This enables switching between tabs and maintaining the mode they were in ## Screenshots (if applicable) <img width="1866" height="1455" alt="image" src="https://github.com/user-attachments/assets/f9a8cd36-181f-4948-b48c-dd27bd9127cf" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9137-App-mode-more-updates-fixes-3106d73d365081a18ccff6ffe24fdec7) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { defineStore } from 'pinia'
|
||||
import { computed, markRaw, ref, shallowRef } from 'vue'
|
||||
import type { Raw } from 'vue'
|
||||
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
|
||||
import type { Point, Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
@@ -44,10 +44,11 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
// Reactive scale percentage that syncs with app.canvas.ds.scale
|
||||
const appScalePercentage = ref(100)
|
||||
|
||||
const { isAppMode, setMode } = useAppMode()
|
||||
const linearMode = computed({
|
||||
get: () => useAppModeStore().isAppMode,
|
||||
get: () => isAppMode.value,
|
||||
set: (val: boolean) => {
|
||||
useAppModeStore().setMode(val ? 'app' : 'graph')
|
||||
setMode(val ? 'app' : 'graph')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appModeStore = useAppModeStore()
|
||||
const { setMode } = useAppMode()
|
||||
const { hasOutputs } = storeToRefs(useAppModeStore())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="appModeStore.hasOutputs"
|
||||
v-if="hasOutputs"
|
||||
role="article"
|
||||
data-testid="arrange-preview"
|
||||
class="flex flex-col items-center justify-center h-full w-3/4 gap-6 p-8 mx-auto"
|
||||
@@ -47,11 +50,7 @@ const appModeStore = useAppModeStore()
|
||||
<p class="mt-0 p-0">{{ t('linearMode.arrange.outputExamples') }}</p>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
@click="appModeStore.setMode('builder:select')"
|
||||
>
|
||||
<Button variant="primary" size="lg" @click="setMode('builder:select')">
|
||||
{{ t('linearMode.arrange.switchToSelectButton') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
@@ -35,7 +36,9 @@ const { batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
const settingStore = useSettingStore()
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { isBuilderMode } = useAppMode()
|
||||
const appModeStore = useAppModeStore()
|
||||
const { hasOutputs } = storeToRefs(appModeStore)
|
||||
|
||||
const props = defineProps<{
|
||||
toastTo?: string | HTMLElement
|
||||
@@ -168,7 +171,7 @@ defineExpose({ runButtonClick })
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-if="!appModeStore.isBuilderMode && appModeStore.hasOutputs"
|
||||
v-if="!isBuilderMode && hasOutputs"
|
||||
class="flex flex-col min-w-80 md:h-full"
|
||||
>
|
||||
<section
|
||||
@@ -299,7 +302,7 @@ defineExpose({ runButtonClick })
|
||||
<Button
|
||||
v-else
|
||||
variant="primary"
|
||||
class="w-full mt-4"
|
||||
class="w-full mt-4 text-sm"
|
||||
size="lg"
|
||||
@click="runButtonClick"
|
||||
>
|
||||
@@ -315,14 +318,18 @@ defineExpose({ runButtonClick })
|
||||
:to="toastTo"
|
||||
>
|
||||
<div
|
||||
class="bg-base-foreground text-base-background rounded-sm flex h-8 p-1 pr-2 gap-2 items-center"
|
||||
class="bg-secondary-background text-base-foreground rounded-lg flex h-8 p-1 pr-2 gap-2 items-center"
|
||||
>
|
||||
<i
|
||||
v-if="jobFinishedQueue"
|
||||
class="icon-[lucide--check] size-5 bg-success-background"
|
||||
class="icon-[lucide--check] size-5 text-muted-foreground"
|
||||
/>
|
||||
<i v-else class="icon-[lucide--loader-circle] size-4 animate-spin" />
|
||||
<span v-text="t('queue.jobAddedToQueue')" />
|
||||
<span
|
||||
v-text="
|
||||
jobFinishedQueue ? t('queue.jobAddedToQueue') : t('queue.jobQueueing')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
@@ -27,13 +27,13 @@ import { useQueueStore } from '@/stores/queueStore'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||
import { executeWidgetsCallback } from '@/utils/litegraphUtil'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const mediaActions = useMediaAssetActions()
|
||||
const queueStore = useQueueStore()
|
||||
const appModeStore = useAppModeStore()
|
||||
const { mode: appModeValue } = useAppMode()
|
||||
const { runButtonClick } = defineProps<{
|
||||
runButtonClick?: (e: Event) => void
|
||||
mobile?: boolean
|
||||
@@ -165,7 +165,7 @@ async function rerun(e: Event) {
|
||||
:model-url="selectedOutput!.url"
|
||||
/>
|
||||
<LatentPreview v-else-if="queueStore.runningTasks.length > 0" />
|
||||
<LinearArrange v-else-if="appModeStore.mode === 'builder:arrange'" />
|
||||
<LinearArrange v-else-if="appModeValue === 'builder:arrange'" />
|
||||
<LinearWelcome v-else />
|
||||
<OutputHistory @update-selection="handleSelection" />
|
||||
<OutputHistory class="not-md:mx-40" @update-selection="handleSelection" />
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appModeStore = useAppModeStore()
|
||||
const { setMode } = useAppMode()
|
||||
const { hasOutputs } = storeToRefs(useAppModeStore())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,7 +27,7 @@ const appModeStore = useAppModeStore()
|
||||
<p class="mt-0">{{ t('linearMode.welcome.controls') }}</p>
|
||||
<p class="mt-0">{{ t('linearMode.welcome.sharing') }}</p>
|
||||
</div>
|
||||
<div v-if="appModeStore.hasOutputs" class="flex flex-row gap-2 text-[14px]">
|
||||
<div v-if="hasOutputs" class="flex flex-row gap-2 text-[14px]">
|
||||
<p class="mt-0 text-base-foreground">
|
||||
<i18n-t keypath="linearMode.welcome.getStarted" tag="span">
|
||||
<template #runButton>
|
||||
@@ -38,18 +41,10 @@ const appModeStore = useAppModeStore()
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="flex flex-row gap-2">
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="lg"
|
||||
@click="appModeStore.setMode('graph')"
|
||||
>
|
||||
<Button variant="textonly" size="lg" @click="setMode('graph')">
|
||||
{{ t('linearMode.welcome.backToWorkflow') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
@click="appModeStore.setMode('builder:select')"
|
||||
>
|
||||
<Button variant="primary" size="lg" @click="setMode('builder:select')">
|
||||
<i class="icon-[lucide--hammer]" />
|
||||
{{ t('linearMode.welcome.buildApp') }}
|
||||
<div
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
|
||||
import { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import OutputHistoryActiveQueueItem from '@/renderer/extensions/linearMode/OutputHistoryActiveQueueItem.vue'
|
||||
import OutputHistoryItem from '@/renderer/extensions/linearMode/OutputHistoryItem.vue'
|
||||
import { useLinearOutputStore } from '@/renderer/extensions/linearMode/linearOutputStore'
|
||||
import type {
|
||||
@@ -275,7 +276,7 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
|
||||
orientation="horizontal"
|
||||
selection-behavior="replace"
|
||||
by="id"
|
||||
class="min-w-0"
|
||||
class="min-w-0 px-4 pb-4"
|
||||
@update:model-value="onSelectionChange"
|
||||
>
|
||||
<ListboxContent as-child>
|
||||
@@ -287,23 +288,7 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
|
||||
>
|
||||
<div class="flex items-center gap-0.5 mx-auto w-fit">
|
||||
<div v-if="queueCount > 0" class="shrink-0 flex items-center gap-0.5">
|
||||
<div
|
||||
class="shrink-0 p-1 border-2 border-transparent relative"
|
||||
data-testid="linear-job"
|
||||
>
|
||||
<div
|
||||
class="size-10 rounded-sm bg-secondary-background flex items-center justify-center"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="queueCount > 1"
|
||||
class="absolute top-0 right-0 min-w-4 h-4 flex justify-center items-center rounded-full bg-primary-background text-text-primary text-xs"
|
||||
v-text="queueCount"
|
||||
/>
|
||||
</div>
|
||||
<OutputHistoryActiveQueueItem :queue-count="queueCount" />
|
||||
<div
|
||||
v-if="hasActiveContent || visibleHistory.length > 0"
|
||||
class="border-l border-border-default h-12 shrink-0 mx-4"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const { queueCount } = defineProps<{
|
||||
queueCount: number
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
function clearQueue(close: () => void) {
|
||||
void commandStore.execute('Comfy.ClearPendingTasks')
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="shrink-0 p-1 border-2 border-transparent relative"
|
||||
data-testid="linear-job"
|
||||
>
|
||||
<Popover side="top" :show-arrow="false" @focus-outside.prevent>
|
||||
<template #button>
|
||||
<Button
|
||||
v-tooltip.top="t('linearMode.queue.clickToClear')"
|
||||
:aria-label="t('linearMode.queue.clickToClear')"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
class="size-10 rounded-sm bg-secondary-background flex items-center justify-center"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<template #default="{ close }">
|
||||
<Button
|
||||
:disabled="queueCount === 0"
|
||||
variant="textonly"
|
||||
class="text-destructive-background px-4 text-sm"
|
||||
@click="clearQueue(close)"
|
||||
>
|
||||
<i class="icon-[lucide--list-x]" />
|
||||
{{ t('linearMode.queue.clear') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Popover>
|
||||
<div
|
||||
v-if="queueCount > 1"
|
||||
aria-hidden="true"
|
||||
class="absolute top-0 right-0 min-w-4 h-4 flex justify-center items-center rounded-full bg-primary-background text-text-primary text-xs"
|
||||
v-text="queueCount"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,11 +14,9 @@ const { apiTarget } = vi.hoisted(() => ({
|
||||
apiTarget: new EventTarget()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/appModeStore', () => ({
|
||||
useAppModeStore: () => ({
|
||||
get isAppMode() {
|
||||
return isAppModeRef.value
|
||||
}
|
||||
vi.mock('@/composables/useAppMode', () => ({
|
||||
useAppMode: () => ({
|
||||
isAppMode: isAppModeRef
|
||||
})
|
||||
}))
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import { flattenNodeOutput } from '@/renderer/extensions/linearMode/flattenNodeO
|
||||
import type { InProgressItem } from '@/renderer/extensions/linearMode/linearModeTypes'
|
||||
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useJobPreviewStore } from '@/stores/jobPreviewStore'
|
||||
|
||||
export const useLinearOutputStore = defineStore('linearOutput', () => {
|
||||
const appModeStore = useAppModeStore()
|
||||
const { isAppMode } = useAppMode()
|
||||
const executionStore = useExecutionStore()
|
||||
const jobPreviewStore = useJobPreviewStore()
|
||||
|
||||
@@ -220,7 +220,7 @@ export const useLinearOutputStore = defineStore('linearOutput', () => {
|
||||
watch(
|
||||
() => executionStore.activeJobId,
|
||||
(jobId, oldJobId) => {
|
||||
if (!appModeStore.isAppMode) return
|
||||
if (!isAppMode.value) return
|
||||
if (oldJobId && oldJobId !== jobId) {
|
||||
onJobComplete(oldJobId)
|
||||
}
|
||||
@@ -233,7 +233,7 @@ export const useLinearOutputStore = defineStore('linearOutput', () => {
|
||||
watch(
|
||||
() => jobPreviewStore.previewsByPromptId,
|
||||
(previews) => {
|
||||
if (!appModeStore.isAppMode) return
|
||||
if (!isAppMode.value) return
|
||||
const jobId = executionStore.activeJobId
|
||||
if (!jobId) return
|
||||
const url = previews[jobId]
|
||||
@@ -243,7 +243,7 @@ export const useLinearOutputStore = defineStore('linearOutput', () => {
|
||||
)
|
||||
|
||||
watch(
|
||||
() => appModeStore.isAppMode,
|
||||
isAppMode,
|
||||
(active, wasActive) => {
|
||||
if (active) {
|
||||
api.addEventListener('executed', handleExecuted)
|
||||
|
||||
Reference in New Issue
Block a user