mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 14:54:12 +00:00
Linear mode arrangement tweaks (#8853)
Planning to keep updates smaller and more contained in the interest of collaboration and velocity - The breadcrumb hamburger menu that provides workflow options is now displayed in linear mode - As part of this change, the reka-ui popover component now accepts primvevue format MenuItems - I prefer the format I had, but this makes transitioning stuff easier. - The simplified linear history is moved to always be horizontal and shown beneath previews. - The label has been removed from the "Give Feedback" button on desktop so it does not overlap - The full side toolbar is displayed in linear mode - This is temporary, but it gets the dead code pruned out now. - Lays some groundwork for selecting an asset from the assets panel to also select the item in the main linear panel - The api `promptQueued` event can now optionally include a promptIds, which list the ids for all jobs that were queued together as part of that batch - Update the max for the `number of generations` field to respect the recently updated cloud limits | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/e632679c-d727-4882-841b-09e99a2f81a4" /> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/a9bcd809-c314-49bd-a479-2448d1a88456"/>| ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8853-Linear-mode-arrangement-tweaks-3066d73d365081589355ef753513900b) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<SidebarTabTemplate
|
||||
:title="isInFolderView ? '' : $t('sideToolbar.mediaAssets.title')"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template #alt-title>
|
||||
<div
|
||||
@@ -272,6 +273,8 @@ const { activeJobsCount } = storeToRefs(queueStore)
|
||||
const executionStore = useExecutionStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const emit = defineEmits<{ assetSelected: [asset: AssetItem] }>()
|
||||
|
||||
const activeTab = ref<'input' | 'output'>('output')
|
||||
const folderJobId = ref<string | null>(null)
|
||||
const folderExecutionTime = ref<number | undefined>(undefined)
|
||||
@@ -530,6 +533,7 @@ watch(
|
||||
function handleAssetSelect(asset: AssetItem, assets?: AssetItem[]) {
|
||||
const assetList = assets ?? visibleAssets.value
|
||||
const index = assetList.findIndex((a) => a.id === asset.id)
|
||||
emit('assetSelected', asset)
|
||||
handleAssetClick(asset, index, assetList)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import {
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
@@ -15,7 +16,7 @@ defineOptions({
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
entries?: { label: string; action?: () => void; icon?: string }[][]
|
||||
entries?: MenuItem[]
|
||||
icon?: string
|
||||
to?: string | HTMLElement
|
||||
}>()
|
||||
@@ -40,33 +41,34 @@ defineProps<{
|
||||
>
|
||||
<slot>
|
||||
<div class="flex flex-col p-1">
|
||||
<section
|
||||
v-for="(entryGroup, index) in entries ?? []"
|
||||
:key="index"
|
||||
class="flex flex-col border-b-2 last:border-none border-border-subtle"
|
||||
>
|
||||
<template v-for="item in entries ?? []" :key="item.label">
|
||||
<div
|
||||
v-for="{ label, action, icon } in entryGroup"
|
||||
:key="label"
|
||||
v-if="item.separator"
|
||||
class="border-b w-full border-border-subtle"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-row gap-4 p-2 rounded-sm my-1',
|
||||
action &&
|
||||
'cursor-pointer hover:bg-secondary-background-hover'
|
||||
item.disabled
|
||||
? 'opacity-50 pointer-events-none'
|
||||
: item.command &&
|
||||
'cursor-pointer hover:bg-secondary-background-hover'
|
||||
)
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
if (!action) return
|
||||
action()
|
||||
(e) => {
|
||||
if (!item.command || item.disabled) return
|
||||
item.command({ originalEvent: e, item })
|
||||
close()
|
||||
}
|
||||
"
|
||||
>
|
||||
<i v-if="icon" :class="icon" />
|
||||
{{ label }}
|
||||
<i v-if="item.icon" :class="item.icon" />
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</slot>
|
||||
<PopoverArrow class="fill-base-background stroke-border-subtle" />
|
||||
|
||||
@@ -11,7 +11,7 @@ import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
|
||||
@@ -30,6 +30,7 @@ const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const { batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
const settingStore = useSettingStore()
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
@@ -101,7 +102,11 @@ const partitionedNodes = computed(() => {
|
||||
})
|
||||
|
||||
const batchCountWidget: SimplifiedWidget<number> = {
|
||||
options: { precision: 0, min: 1, max: isCloud ? 4 : 99 },
|
||||
options: {
|
||||
precision: 0,
|
||||
min: 1,
|
||||
max: settingStore.get('Comfy.QueueButton.BatchCountLimit')
|
||||
},
|
||||
value: 1,
|
||||
name: t('linearMode.runCount'),
|
||||
type: 'number'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
@@ -12,6 +13,7 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
|
||||
import { extractWorkflowFromAsset } from '@/platform/workflow/utils/workflowExtractionUtil'
|
||||
import ImagePreview from '@/renderer/extensions/linearMode/ImagePreview.vue'
|
||||
import LinearWelcome from '@/renderer/extensions/linearMode/LinearWelcome.vue'
|
||||
import OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue'
|
||||
// Lazy-loaded to avoid pulling THREE.js into the main bundle
|
||||
const Preview3d = () => import('@/renderer/extensions/linearMode/Preview3d.vue')
|
||||
import VideoPreview from '@/renderer/extensions/linearMode/VideoPreview.vue'
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
} from '@/renderer/extensions/linearMode/mediaTypes'
|
||||
import type { StatItem } from '@/renderer/extensions/linearMode/mediaTypes'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { formatDuration } from '@/utils/dateTimeUtil'
|
||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||
@@ -28,15 +31,22 @@ import { executeWidgetsCallback } from '@/utils/litegraphUtil'
|
||||
|
||||
const { t, d } = useI18n()
|
||||
const mediaActions = useMediaAssetActions()
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
const { runButtonClick, selectedItem, selectedOutput } = defineProps<{
|
||||
latentPreview?: string
|
||||
const { runButtonClick } = defineProps<{
|
||||
runButtonClick?: (e: Event) => void
|
||||
selectedItem?: AssetItem
|
||||
selectedOutput?: ResultItemImpl
|
||||
mobile?: boolean
|
||||
}>()
|
||||
|
||||
const selectedItem = ref<AssetItem>()
|
||||
const selectedOutput = ref<ResultItemImpl>()
|
||||
const canShowPreview = ref(true)
|
||||
const latentPreview = ref<string>()
|
||||
whenever(
|
||||
() => nodeOutputStore.latestPreview[0],
|
||||
() => (latentPreview.value = nodeOutputStore.latestPreview[0])
|
||||
)
|
||||
|
||||
const dateOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
@@ -55,16 +65,17 @@ function formatTime(time?: string) {
|
||||
}
|
||||
|
||||
const itemStats = computed<StatItem[]>(() => {
|
||||
if (!selectedItem) return []
|
||||
const user_metadata = getOutputAssetMetadata(selectedItem.user_metadata)
|
||||
if (!selectedItem.value) return []
|
||||
const user_metadata = getOutputAssetMetadata(selectedItem.value.user_metadata)
|
||||
if (!user_metadata) return []
|
||||
|
||||
const { allOutputs } = user_metadata
|
||||
return [
|
||||
{ content: formatTime(selectedItem.created_at) },
|
||||
{ content: formatTime(selectedItem.value.created_at) },
|
||||
{ content: formatDuration(user_metadata.executionTimeInSeconds) },
|
||||
allOutputs && { content: t('g.asset', allOutputs.length) },
|
||||
(selectedOutput && mediaTypes[getMediaType(selectedOutput)]) ?? {}
|
||||
(selectedOutput.value && mediaTypes[getMediaType(selectedOutput.value)]) ??
|
||||
{}
|
||||
].filter((i) => !!i)
|
||||
})
|
||||
|
||||
@@ -89,7 +100,7 @@ async function loadWorkflow(item: AssetItem | undefined) {
|
||||
|
||||
async function rerun(e: Event) {
|
||||
if (!runButtonClick) return
|
||||
await loadWorkflow(selectedItem)
|
||||
await loadWorkflow(selectedItem.value)
|
||||
//FIXME don't use timeouts here
|
||||
//Currently seeds fail to properly update even with timeouts?
|
||||
await new Promise((r) => setTimeout(r, 500))
|
||||
@@ -136,28 +147,28 @@ async function rerun(e: Event) {
|
||||
</Button>
|
||||
<Popover
|
||||
:entries="[
|
||||
[
|
||||
{
|
||||
icon: 'icon-[lucide--download]',
|
||||
label: t('linearMode.downloadAll'),
|
||||
action: () => downloadAsset(selectedItem!)
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
icon: 'icon-[lucide--trash-2]',
|
||||
label: t('queue.jobMenu.deleteAsset'),
|
||||
action: () => mediaActions.deleteAssets(selectedItem!)
|
||||
}
|
||||
]
|
||||
{
|
||||
icon: 'icon-[lucide--download]',
|
||||
label: t('linearMode.downloadAll'),
|
||||
command: () => downloadAsset(selectedItem!)
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
icon: 'icon-[lucide--trash-2]',
|
||||
label: t('queue.jobMenu.deleteAsset'),
|
||||
command: () => mediaActions.deleteAssets(selectedItem!)
|
||||
}
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<ImagePreview
|
||||
v-if="latentPreview ?? getMediaType(selectedOutput) === 'images'"
|
||||
v-if="
|
||||
(canShowPreview && latentPreview) ||
|
||||
getMediaType(selectedOutput) === 'images'
|
||||
"
|
||||
:mobile
|
||||
:src="latentPreview ?? selectedOutput!.url"
|
||||
:src="(canShowPreview && latentPreview) || selectedOutput!.url"
|
||||
/>
|
||||
<VideoPreview
|
||||
v-else-if="getMediaType(selectedOutput) === 'video'"
|
||||
@@ -180,4 +191,12 @@ async function rerun(e: Event) {
|
||||
:model-url="selectedOutput!.url"
|
||||
/>
|
||||
<LinearWelcome v-else />
|
||||
<OutputHistory
|
||||
@update-selection="
|
||||
(event) => {
|
||||
;[selectedItem, selectedOutput, canShowPreview] = event
|
||||
latentPreview = undefined
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -2,24 +2,17 @@
|
||||
import {
|
||||
useAsyncState,
|
||||
useEventListener,
|
||||
useInfiniteScroll,
|
||||
useScroll
|
||||
useInfiniteScroll
|
||||
} from '@vueuse/core'
|
||||
import { computed, ref, toRaw, toValue, useTemplateRef, watch } from 'vue'
|
||||
import type { MaybeRef } from 'vue'
|
||||
|
||||
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
||||
import SidebarIcon from '@/components/sidebar/SidebarIcon.vue'
|
||||
import SidebarTemplatesButton from '@/components/sidebar/SidebarTemplatesButton.vue'
|
||||
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
|
||||
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||
import { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
|
||||
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import {
|
||||
getMediaType,
|
||||
mediaTypes
|
||||
@@ -27,10 +20,8 @@ import {
|
||||
import type { NodeExecutionOutput, ResultItem } from '@/schemas/apiSchema'
|
||||
import { getJobDetail } from '@/services/jobOutputCache'
|
||||
import { useQueueStore, ResultItemImpl } from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const displayWorkflows = ref(false)
|
||||
const outputs = useMediaAssets('output')
|
||||
const {
|
||||
progressBarContainerClass,
|
||||
@@ -40,26 +31,15 @@ const {
|
||||
} = useProgressBarBackground()
|
||||
const { totalPercent, currentNodePercent } = useQueueProgress()
|
||||
const queueStore = useQueueStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const workflowTab = useWorkspaceStore()
|
||||
.getSidebarTabs()
|
||||
.find((w) => w.id === 'workflows')
|
||||
|
||||
void outputs.fetchMediaList()
|
||||
|
||||
defineProps<{
|
||||
scrollResetButtonTo?: string | HTMLElement
|
||||
mobile?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
updateSelection: [
|
||||
selection: [AssetItem | undefined, ResultItemImpl | undefined, boolean]
|
||||
]
|
||||
}>()
|
||||
|
||||
defineExpose({ onWheel })
|
||||
|
||||
const selectedIndex = ref<[number, number]>([-1, 0])
|
||||
|
||||
function doEmit() {
|
||||
@@ -72,17 +52,9 @@ function doEmit() {
|
||||
}
|
||||
|
||||
const outputsRef = useTemplateRef('outputsRef')
|
||||
const { reset: resetInfiniteScroll } = useInfiniteScroll(
|
||||
outputsRef,
|
||||
outputs.loadMore,
|
||||
{ canLoadMore: () => outputs.hasMore.value }
|
||||
)
|
||||
function resetOutputsScroll() {
|
||||
//TODO need to also prune outputs entries?
|
||||
resetInfiniteScroll()
|
||||
outputsRef.value?.scrollTo(0, 0)
|
||||
}
|
||||
const { y: outputScrollState } = useScroll(outputsRef)
|
||||
useInfiniteScroll(outputsRef, outputs.loadMore, {
|
||||
canLoadMore: () => outputs.hasMore.value
|
||||
})
|
||||
|
||||
watch(selectedIndex, () => {
|
||||
const [index, key] = selectedIndex.value
|
||||
@@ -208,26 +180,31 @@ function gotoPreviousOutput() {
|
||||
|
||||
let pointer = new CanvasPointer(document.body)
|
||||
let scrollOffset = 0
|
||||
function onWheel(e: WheelEvent) {
|
||||
if (!e.ctrlKey && !e.metaKey) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
useEventListener(
|
||||
document.body,
|
||||
'wheel',
|
||||
function (e: WheelEvent) {
|
||||
if (!e.ctrlKey && !e.metaKey) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (!pointer.isTrackpadGesture(e)) {
|
||||
if (e.deltaY > 0) gotoNextOutput()
|
||||
else gotoPreviousOutput()
|
||||
return
|
||||
}
|
||||
scrollOffset += e.deltaY
|
||||
while (scrollOffset >= 60) {
|
||||
scrollOffset -= 60
|
||||
gotoNextOutput()
|
||||
}
|
||||
while (scrollOffset <= -60) {
|
||||
scrollOffset += 60
|
||||
gotoPreviousOutput()
|
||||
}
|
||||
}
|
||||
if (!pointer.isTrackpadGesture(e)) {
|
||||
if (e.deltaY > 0) gotoNextOutput()
|
||||
else gotoPreviousOutput()
|
||||
return
|
||||
}
|
||||
scrollOffset += e.deltaY
|
||||
while (scrollOffset >= 60) {
|
||||
scrollOffset -= 60
|
||||
gotoNextOutput()
|
||||
}
|
||||
while (scrollOffset <= -60) {
|
||||
scrollOffset += 60
|
||||
gotoPreviousOutput()
|
||||
}
|
||||
},
|
||||
{ capture: true, passive: false }
|
||||
)
|
||||
|
||||
useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
|
||||
if (
|
||||
@@ -244,138 +221,80 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'min-w-38 flex bg-comfy-menu-bg md:h-full border-border-subtle',
|
||||
settingStore.get('Comfy.Sidebar.Location') === 'right'
|
||||
? 'flex-row-reverse border-l'
|
||||
: 'md:border-r'
|
||||
)
|
||||
"
|
||||
v-bind="$attrs"
|
||||
<article
|
||||
ref="outputsRef"
|
||||
data-testid="linear-outputs"
|
||||
class="h-24 p-3 overflow-x-auto overflow-y-clip border-node-component-border flex items-center contain-size md:mx-15"
|
||||
>
|
||||
<div
|
||||
v-if="!mobile"
|
||||
class="h-full flex flex-col w-14 shrink-0 overflow-hidden items-center p-2"
|
||||
<section
|
||||
v-if="
|
||||
queueStore.runningTasks.length > 0 || queueStore.pendingTasks.length > 0
|
||||
"
|
||||
data-testid="linear-job"
|
||||
class="py-3 h-24 aspect-square px-1 relative"
|
||||
>
|
||||
<template v-if="workflowTab">
|
||||
<SidebarIcon
|
||||
:icon="workflowTab.icon"
|
||||
:icon-badge="workflowTab.iconBadge"
|
||||
:tooltip="workflowTab.tooltip"
|
||||
:label="workflowTab.label || workflowTab.title"
|
||||
:class="workflowTab.id + '-tab-button'"
|
||||
:selected="displayWorkflows"
|
||||
:is-small="settingStore.get('Comfy.Sidebar.Size') === 'small'"
|
||||
@click="displayWorkflows = !displayWorkflows"
|
||||
/>
|
||||
</template>
|
||||
<SidebarTemplatesButton />
|
||||
<div class="flex-1" />
|
||||
<ModeToggle />
|
||||
</div>
|
||||
<div class="border-border-subtle md:border-r" />
|
||||
<WorkflowsSidebarTab v-if="displayWorkflows" class="min-w-50 grow-1" />
|
||||
<article
|
||||
v-else
|
||||
ref="outputsRef"
|
||||
data-testid="linear-outputs"
|
||||
class="h-24 md:h-full min-w-24 grow-1 p-3 overflow-x-auto overflow-y-clip md:overflow-y-auto md:overflow-x-clip md:border-r-1 border-node-component-border flex md:flex-col items-center contain-size"
|
||||
>
|
||||
<section
|
||||
<i
|
||||
v-if="queueStore.runningTasks.length > 0"
|
||||
class="icon-[lucide--loader-circle] size-full animate-spin"
|
||||
/>
|
||||
<i v-else class="icon-[lucide--ellipsis] size-full animate-pulse" />
|
||||
<div
|
||||
v-if="
|
||||
queueStore.runningTasks.length > 0 ||
|
||||
queueStore.pendingTasks.length > 0
|
||||
queueStore.runningTasks.length + queueStore.pendingTasks.length > 1
|
||||
"
|
||||
data-testid="linear-job"
|
||||
class="py-3 not-md:h-24 md:w-full aspect-square px-1 relative"
|
||||
>
|
||||
<i
|
||||
v-if="queueStore.runningTasks.length > 0"
|
||||
class="icon-[lucide--loader-circle] size-full animate-spin"
|
||||
/>
|
||||
<i v-else class="icon-[lucide--ellipsis] size-full animate-pulse" />
|
||||
<div
|
||||
v-if="
|
||||
queueStore.runningTasks.length + queueStore.pendingTasks.length > 1
|
||||
"
|
||||
class="absolute top-0 right-0 p-1 min-w-5 h-5 flex justify-center items-center rounded-full bg-primary-background text-text-primary"
|
||||
v-text="
|
||||
queueStore.runningTasks.length + queueStore.pendingTasks.length
|
||||
"
|
||||
/>
|
||||
<div class="absolute -bottom-1 w-full h-3 rounded-sm overflow-clip">
|
||||
<div :class="progressBarContainerClass">
|
||||
<div
|
||||
:class="progressBarPrimaryClass"
|
||||
:style="progressPercentStyle(totalPercent)"
|
||||
/>
|
||||
<div
|
||||
:class="progressBarSecondaryClass"
|
||||
:style="progressPercentStyle(currentNodePercent)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<template v-for="(item, index) in outputs.media.value" :key="index">
|
||||
<div
|
||||
class="border-border-subtle not-md:border-l md:border-t first:border-none not-md:h-21 md:w-full m-3"
|
||||
/>
|
||||
<template v-for="(output, key) in toValue(allOutputs(item))" :key>
|
||||
<img
|
||||
v-if="getMediaType(output) === 'images'"
|
||||
:class="
|
||||
cn(
|
||||
'p-1 rounded-lg aspect-square object-cover not-md:h-20 md:w-full',
|
||||
index === selectedIndex[0] &&
|
||||
key === selectedIndex[1] &&
|
||||
'border-2'
|
||||
)
|
||||
"
|
||||
:data-output-index="index"
|
||||
:src="output.url"
|
||||
@click="selectedIndex = [index, key]"
|
||||
class="absolute top-0 right-0 p-1 min-w-5 h-5 flex justify-center items-center rounded-full bg-primary-background text-text-primary"
|
||||
v-text="queueStore.runningTasks.length + queueStore.pendingTasks.length"
|
||||
/>
|
||||
<div class="absolute -bottom-1 w-full h-3 rounded-sm overflow-clip">
|
||||
<div :class="progressBarContainerClass">
|
||||
<div
|
||||
:class="progressBarPrimaryClass"
|
||||
:style="progressPercentStyle(totalPercent)"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:class="progressBarSecondaryClass"
|
||||
:style="progressPercentStyle(currentNodePercent)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<template v-for="(item, index) in outputs.media.value" :key="index">
|
||||
<div class="border-border-subtle border-l first:border-none h-21 m-3" />
|
||||
<template v-for="(output, key) in toValue(allOutputs(item))" :key>
|
||||
<img
|
||||
v-if="getMediaType(output) === 'images'"
|
||||
:class="
|
||||
cn(
|
||||
'p-1 rounded-lg aspect-square object-cover h-20',
|
||||
index === selectedIndex[0] &&
|
||||
key === selectedIndex[1] &&
|
||||
'border-2'
|
||||
)
|
||||
"
|
||||
:data-output-index="index"
|
||||
:src="output.url"
|
||||
@click="selectedIndex = [index, key]"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:class="
|
||||
cn(
|
||||
'p-1 rounded-lg aspect-square w-full',
|
||||
index === selectedIndex[0] &&
|
||||
key === selectedIndex[1] &&
|
||||
'border-2'
|
||||
)
|
||||
"
|
||||
:data-output-index="index"
|
||||
@click="selectedIndex = [index, key]"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
cn(
|
||||
'p-1 rounded-lg aspect-square w-full',
|
||||
index === selectedIndex[0] &&
|
||||
key === selectedIndex[1] &&
|
||||
'border-2'
|
||||
)
|
||||
cn(mediaTypes[getMediaType(output)]?.iconClass, 'size-full')
|
||||
"
|
||||
:data-output-index="index"
|
||||
@click="selectedIndex = [index, key]"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
cn(mediaTypes[getMediaType(output)]?.iconClass, 'size-full')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</article>
|
||||
</div>
|
||||
<Teleport
|
||||
v-if="outputScrollState && scrollResetButtonTo"
|
||||
:to="scrollResetButtonTo"
|
||||
>
|
||||
<Button
|
||||
:class="
|
||||
cn(
|
||||
'p-3 size-10 bg-base-foreground',
|
||||
settingStore.get('Comfy.Sidebar.Location') === 'left'
|
||||
? 'left-4'
|
||||
: 'right-4'
|
||||
)
|
||||
"
|
||||
@click="resetOutputsScroll"
|
||||
>
|
||||
<i class="icon-[lucide--arrow-up] size-4 bg-base-background" />
|
||||
</Button>
|
||||
</Teleport>
|
||||
</template>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
breakpointsTailwind,
|
||||
unrefElement,
|
||||
useBreakpoints,
|
||||
whenever
|
||||
} from '@vueuse/core'
|
||||
import { breakpointsTailwind, unrefElement, useBreakpoints } from '@vueuse/core'
|
||||
import Splitter from 'primevue/splitter'
|
||||
import SplitterPanel from 'primevue/splitterpanel'
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
import { computed, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { useWorkflowActionsMenu } from '@/composables/useWorkflowActionsMenu'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import LinearControls from '@/renderer/extensions/linearMode/LinearControls.vue'
|
||||
import LinearPreview from '@/renderer/extensions/linearMode/LinearPreview.vue'
|
||||
import MobileMenu from '@/renderer/extensions/linearMode/MobileMenu.vue'
|
||||
import OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
const settingStore = useSettingStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
|
||||
const mobileDisplay = useBreakpoints(breakpointsTailwind).smaller('md')
|
||||
|
||||
const hasPreview = ref(false)
|
||||
whenever(
|
||||
() => nodeOutputStore.latestPreview[0],
|
||||
() => (hasPreview.value = true)
|
||||
)
|
||||
const activeTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
||||
|
||||
const selectedItem = ref<AssetItem>()
|
||||
const selectedOutput = ref<ResultItemImpl>()
|
||||
const canShowPreview = ref(true)
|
||||
const outputHistoryRef = useTemplateRef('outputHistoryRef')
|
||||
const { menuItems } = useWorkflowActionsMenu(
|
||||
() => useCommandStore().execute('Comfy.RenameWorkflow'),
|
||||
{ isRoot: true }
|
||||
)
|
||||
|
||||
const topLeftRef = useTemplateRef('topLeftRef')
|
||||
const topRightRef = useTemplateRef('topRightRef')
|
||||
@@ -47,10 +41,7 @@ const bottomRightRef = useTemplateRef('bottomRightRef')
|
||||
const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="absolute w-full h-full"
|
||||
@wheel.capture="(e: WheelEvent) => outputHistoryRef?.onWheel(e)"
|
||||
>
|
||||
<div class="absolute w-full h-full">
|
||||
<div class="workflow-tabs-container pointer-events-auto h-9.5 w-full">
|
||||
<div class="flex h-full items-center">
|
||||
<WorkflowTabs />
|
||||
@@ -64,29 +55,10 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
<MobileMenu />
|
||||
<div class="flex flex-col text-muted-foreground">
|
||||
<LinearPreview
|
||||
:latent-preview="
|
||||
canShowPreview && hasPreview
|
||||
? nodeOutputStore.latestPreview[0]
|
||||
: undefined
|
||||
"
|
||||
:run-button-click="linearWorkflowRef?.runButtonClick"
|
||||
:selected-item
|
||||
:selected-output
|
||||
mobile
|
||||
/>
|
||||
</div>
|
||||
<OutputHistory
|
||||
ref="outputHistoryRef"
|
||||
mobile
|
||||
@update-selection="
|
||||
([item, output, canShow]) => {
|
||||
selectedItem = item
|
||||
selectedOutput = output
|
||||
canShowPreview = canShow
|
||||
hasPreview = false
|
||||
}
|
||||
"
|
||||
/>
|
||||
<LinearControls ref="linearWorkflowRef" mobile />
|
||||
<div class="text-base-foreground flex items-center gap-4">
|
||||
<div class="border-r border-border-subtle mr-auto">
|
||||
@@ -99,7 +71,7 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
<Splitter
|
||||
v-else
|
||||
class="h-[calc(100%-38px)] w-full bg-comfy-menu-secondary-bg"
|
||||
:pt="{ gutter: { class: 'bg-transparent w-4 -mx-3' } }"
|
||||
:pt="{ gutter: { class: 'bg-transparent w-4 -mx-1' } }"
|
||||
@resizestart="({ originalEvent }) => originalEvent.preventDefault()"
|
||||
>
|
||||
<SplitterPanel
|
||||
@@ -107,50 +79,41 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
:size="1"
|
||||
class="min-w-min outline-none"
|
||||
>
|
||||
<OutputHistory
|
||||
<div
|
||||
v-if="settingStore.get('Comfy.Sidebar.Location') === 'left'"
|
||||
ref="outputHistoryRef"
|
||||
:scroll-reset-button-to="unrefElement(bottomLeftRef) ?? undefined"
|
||||
@update-selection="
|
||||
([item, output, canShow]) => {
|
||||
selectedItem = item
|
||||
selectedOutput = output
|
||||
canShowPreview = canShow
|
||||
hasPreview = false
|
||||
}
|
||||
"
|
||||
/>
|
||||
class="flex h-full border-border-subtle border-r"
|
||||
>
|
||||
<SideToolbar />
|
||||
<ExtensionSlot v-if="activeTab" :extension="activeTab" />
|
||||
</div>
|
||||
<LinearControls
|
||||
v-else
|
||||
ref="linearWorkflowRef"
|
||||
:toast-to="unrefElement(bottomLeftRef) ?? undefined"
|
||||
:notes-to="unrefElement(topLeftRef) ?? undefined"
|
||||
/>
|
||||
<div />
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
id="linearCenterPanel"
|
||||
:size="98"
|
||||
class="flex flex-col min-w-min gap-4 mx-2 px-10 pt-8 pb-4 relative text-muted-foreground outline-none"
|
||||
>
|
||||
<LinearPreview
|
||||
:latent-preview="
|
||||
canShowPreview && hasPreview
|
||||
? nodeOutputStore.latestPreview[0]
|
||||
: undefined
|
||||
"
|
||||
:run-button-click="linearWorkflowRef?.runButtonClick"
|
||||
:selected-item
|
||||
:selected-output
|
||||
/>
|
||||
<div ref="topLeftRef" class="absolute z-21 top-4 left-4" />
|
||||
<LinearPreview :run-button-click="linearWorkflowRef?.runButtonClick" />
|
||||
<div ref="topLeftRef" class="absolute z-21 top-4 left-4">
|
||||
<Popover :entries="menuItems" align="start">
|
||||
<template #button>
|
||||
<Button size="icon" variant="textonly">
|
||||
<i class="icon-[lucide--menu]" />
|
||||
</Button>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
<div ref="topRightRef" class="absolute z-21 top-4 right-4" />
|
||||
<div ref="bottomLeftRef" class="absolute z-20 bottom-4 left-4" />
|
||||
<div ref="bottomRightRef" class="absolute z-20 bottom-24 right-4" />
|
||||
<div
|
||||
class="absolute z-20 bottom-4 right-4 text-base-foreground flex items-center gap-4"
|
||||
>
|
||||
<div v-text="t('linearMode.beta')" />
|
||||
<TypeformPopoverButton
|
||||
data-tf-widget="gmVqFi8l"
|
||||
:align="
|
||||
@@ -172,20 +135,10 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
:toast-to="unrefElement(bottomRightRef) ?? undefined"
|
||||
:notes-to="unrefElement(topRightRef) ?? undefined"
|
||||
/>
|
||||
<OutputHistory
|
||||
v-else
|
||||
ref="outputHistoryRef"
|
||||
:scroll-reset-button-to="unrefElement(bottomRightRef) ?? undefined"
|
||||
@update-selection="
|
||||
([item, output, canShow]) => {
|
||||
selectedItem = item
|
||||
selectedOutput = output
|
||||
canShowPreview = canShow
|
||||
hasPreview = false
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div />
|
||||
<div v-else class="flex h-full border-border-subtle border-l">
|
||||
<ExtensionSlot v-if="activeTab" :extension="activeTab" />
|
||||
<SideToolbar class="border-border-subtle border-l" />
|
||||
</div>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user