diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index 551e26c62..df8fe4a23 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -247,6 +247,7 @@ --inverted-background-hover: var(--color-charcoal-600); --warning-background: var(--color-gold-400); --warning-background-hover: var(--color-gold-500); + --success-background: var(--color-jade-600); --border-default: var(--color-smoke-600); --border-subtle: var(--color-smoke-400); --muted-background: var(--color-smoke-700); @@ -372,6 +373,7 @@ --inverted-background-hover: var(--color-smoke-200); --warning-background: var(--color-gold-600); --warning-background-hover: var(--color-gold-500); + --success-background: var(--color-jade-600); --border-default: var(--color-charcoal-200); --border-subtle: var(--color-charcoal-300); --muted-background: var(--color-charcoal-100); @@ -516,6 +518,7 @@ --color-inverted-background-hover: var(--inverted-background-hover); --color-warning-background: var(--warning-background); --color-warning-background-hover: var(--warning-background-hover); + --color-success-background: var(--success-background); --color-border-default: var(--border-default); --color-border-subtle: var(--border-subtle); --color-muted-background: var(--muted-background); diff --git a/src/components/sidebar/ModeToggle.vue b/src/components/sidebar/ModeToggle.vue new file mode 100644 index 000000000..ca94be771 --- /dev/null +++ b/src/components/sidebar/ModeToggle.vue @@ -0,0 +1,28 @@ + + diff --git a/src/components/sidebar/SideToolbar.vue b/src/components/sidebar/SideToolbar.vue index 8f0d800cb..a8aaad758 100644 --- a/src/components/sidebar/SideToolbar.vue +++ b/src/components/sidebar/SideToolbar.vue @@ -44,6 +44,7 @@ + @@ -51,15 +52,17 @@ + + diff --git a/src/components/ui/TypeformPopoverButton.vue b/src/components/ui/TypeformPopoverButton.vue new file mode 100644 index 000000000..e6808e8bc --- /dev/null +++ b/src/components/ui/TypeformPopoverButton.vue @@ -0,0 +1,29 @@ + + diff --git a/src/components/ui/ZoomPane.vue b/src/components/ui/ZoomPane.vue new file mode 100644 index 000000000..60ea6533b --- /dev/null +++ b/src/components/ui/ZoomPane.vue @@ -0,0 +1,59 @@ + + diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index 786e23f4e..348765d78 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -157,7 +157,7 @@ const normalizeWidgetValue = (value: unknown): WidgetValue => { return undefined } -export function safeWidgetMapper( +function safeWidgetMapper( node: LGraphNode, slotMetadata: Map ): (widget: IBaseWidget) => SafeWidgetData { @@ -207,15 +207,77 @@ export function safeWidgetMapper( } } -export function isValidWidgetValue(value: unknown): value is WidgetValue { - return ( - value === null || - value === undefined || - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' || - typeof value === 'object' - ) +// Extract safe data from LiteGraph node for Vue consumption +export function extractVueNodeData(node: LGraphNode): VueNodeData { + // Determine subgraph ID - null for root graph, string for subgraphs + const subgraphId = + node.graph && 'id' in node.graph && node.graph !== node.graph.rootGraph + ? String(node.graph.id) + : null + // Extract safe widget data + const slotMetadata = new Map() + + const reactiveWidgets = shallowReactive(node.widgets ?? []) + Object.defineProperty(node, 'widgets', { + get() { + return reactiveWidgets + }, + set(v) { + reactiveWidgets.splice(0, reactiveWidgets.length, ...v) + } + }) + const reactiveInputs = shallowReactive(node.inputs ?? []) + Object.defineProperty(node, 'inputs', { + get() { + return reactiveInputs + }, + set(v) { + reactiveInputs.splice(0, reactiveInputs.length, ...v) + } + }) + + const safeWidgets = reactiveComputed(() => { + node.inputs?.forEach((input, index) => { + if (!input?.widget?.name) return + slotMetadata.set(input.widget.name, { + index, + linked: input.link != null + }) + }) + return node.widgets?.map(safeWidgetMapper(node, slotMetadata)) ?? [] + }) + + const nodeType = + node.type || + node.constructor?.comfyClass || + node.constructor?.title || + node.constructor?.name || + 'Unknown' + + const apiNode = node.constructor?.nodeData?.api_node ?? false + const badges = node.badges + + return { + id: String(node.id), + title: typeof node.title === 'string' ? node.title : '', + type: nodeType, + mode: node.mode || 0, + titleMode: node.title_mode, + selected: node.selected || false, + executing: false, // Will be updated separately based on execution state + subgraphId, + apiNode, + badges, + hasErrors: !!node.has_errors, + widgets: safeWidgets, + inputs: reactiveInputs, + outputs: node.outputs ? [...node.outputs] : undefined, + flags: node.flags ? { ...node.flags } : undefined, + color: node.color || undefined, + bgcolor: node.bgcolor || undefined, + resizable: node.resizable, + shape: node.shape + } } export function useGraphNodeManager(graph: LGraph): GraphNodeManager { @@ -251,79 +313,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { } } - // Extract safe data from LiteGraph node for Vue consumption - function extractVueNodeData(node: LGraphNode): VueNodeData { - // Determine subgraph ID - null for root graph, string for subgraphs - const subgraphId = - node.graph && 'id' in node.graph && node.graph !== node.graph.rootGraph - ? String(node.graph.id) - : null - // Extract safe widget data - const slotMetadata = new Map() - - const reactiveWidgets = shallowReactive(node.widgets ?? []) - Object.defineProperty(node, 'widgets', { - get() { - return reactiveWidgets - }, - set(v) { - reactiveWidgets.splice(0, reactiveWidgets.length, ...v) - } - }) - const reactiveInputs = shallowReactive(node.inputs ?? []) - Object.defineProperty(node, 'inputs', { - get() { - return reactiveInputs - }, - set(v) { - reactiveInputs.splice(0, reactiveInputs.length, ...v) - } - }) - - const safeWidgets = reactiveComputed(() => { - node.inputs?.forEach((input, index) => { - if (!input?.widget?.name) return - slotMetadata.set(input.widget.name, { - index, - linked: input.link != null - }) - }) - return node.widgets?.map(safeWidgetMapper(node, slotMetadata)) ?? [] - }) - - const nodeType = - node.type || - node.constructor?.comfyClass || - node.constructor?.title || - node.constructor?.name || - 'Unknown' - - const apiNode = node.constructor?.nodeData?.api_node ?? false - const badges = node.badges - - return { - id: String(node.id), - title: typeof node.title === 'string' ? node.title : '', - type: nodeType, - mode: node.mode || 0, - titleMode: node.title_mode, - selected: node.selected || false, - executing: false, // Will be updated separately based on execution state - subgraphId, - apiNode, - badges, - hasErrors: !!node.has_errors, - widgets: safeWidgets, - inputs: reactiveInputs, - outputs: node.outputs ? [...node.outputs] : undefined, - flags: node.flags ? { ...node.flags } : undefined, - color: node.color || undefined, - bgcolor: node.bgcolor || undefined, - resizable: node.resizable, - shape: node.shape - } - } - // Get access to original LiteGraph node (non-reactive) const getNode = (id: string): LGraphNode | undefined => { return nodeRefs.get(id) diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 2025eb66c..5bd569085 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -1235,7 +1235,12 @@ export function useCoreCommands(): ComfyCommand[] { id: 'Comfy.ToggleLinear', icon: 'pi pi-database', label: 'toggle linear mode', - function: () => (canvasStore.linearMode = !canvasStore.linearMode) + function: () => { + const newMode = !canvasStore.linearMode + app.rootGraph.extra.linearMode = newMode + workflowStore.activeWorkflow?.changeTracker?.checkState() + canvasStore.linearMode = newMode + } } ] diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index 3388140ad..4e7368bc6 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -16,6 +16,7 @@ export enum ServerFeatureFlag { PRIVATE_MODELS_ENABLED = 'private_models_enabled', ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled', HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled', + LINEAR_TOGGLE_ENABLED = 'linear_toggle_enabled', ASYNC_MODEL_UPLOAD_ENABLED = 'async_model_upload_enabled' } @@ -77,6 +78,12 @@ export function useFeatureFlags() { ) ) }, + get linearToggleEnabled() { + return ( + remoteConfig.value.linear_toggle_enabled ?? + api.getServerFeature(ServerFeatureFlag.LINEAR_TOGGLE_ENABLED, false) + ) + }, get asyncModelUploadEnabled() { return ( remoteConfig.value.async_model_upload_enabled ?? diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 81e73b986..5266de220 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -190,6 +190,7 @@ "failed": "Failed", "cancelled": "Cancelled", "job": "Job", + "asset": "{count} assets | {count} asset | {count} assets", "untitled": "Untitled", "emDash": "—", "enabling": "Enabling {id}", @@ -677,7 +678,8 @@ "filterImage": "Image", "filterVideo": "Video", "filterAudio": "Audio", - "filter3D": "3D" + "filter3D": "3D", + "filterText": "Text" }, "backToAssets": "Back to all assets", "searchAssets": "Search Assets", @@ -1182,7 +1184,7 @@ "Experimental: Enable AssetAPI": "Experimental: Enable AssetAPI", "Canvas Performance": "Canvas Performance", "Help Center": "Help Center", - "toggle linear mode": "toggle linear mode", + "toggle linear mode": "toggle simple mode", "Toggle Queue Panel V2": "Toggle Queue Panel V2", "Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)", "Undo": "Undo", @@ -2472,8 +2474,14 @@ "message": "Switch back to Nodes 2.0 anytime from the main menu." }, "linearMode": { - "share": "Share", - "openWorkflow": "Open Workflow" + "linearMode": "Simple Mode", + "beta": "Beta - Give Feedback", + "graphMode": "Graph Mode", + "dragAndDropImage": "Drag and drop an image", + "runCount": "Run count:", + "rerun": "Rerun", + "reuseParameters": "Reuse Parameters", + "downloadAll": "Download All" }, "missingNodes": { "cloud": { diff --git a/src/platform/remoteConfig/types.ts b/src/platform/remoteConfig/types.ts index f4e51115b..7de4c2da2 100644 --- a/src/platform/remoteConfig/types.ts +++ b/src/platform/remoteConfig/types.ts @@ -40,5 +40,6 @@ export type RemoteConfig = { private_models_enabled?: boolean onboarding_survey_enabled?: boolean huggingface_model_import_enabled?: boolean + linear_toggle_enabled?: boolean async_model_upload_enabled?: boolean } diff --git a/src/platform/workflow/core/services/workflowService.ts b/src/platform/workflow/core/services/workflowService.ts index 37c4ef5e5..8dc6cbdcf 100644 --- a/src/platform/workflow/core/services/workflowService.ts +++ b/src/platform/workflow/core/services/workflowService.ts @@ -11,6 +11,7 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' +import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail' import { app } from '@/scripts/app' import { blankGraph, defaultGraph } from '@/scripts/defaultGraph' @@ -311,6 +312,11 @@ export const useWorkflowService = () => { workflowData: ComfyWorkflowJSON ) => { const workflowStore = useWorkspaceStore().workflow + if ( + workflowData.extra?.linearMode !== undefined || + !workflowData.nodes.length + ) + useCanvasStore().linearMode = !!workflowData.extra?.linearMode if (value === null || typeof value === 'string') { const path = value as string | null @@ -332,6 +338,11 @@ export const useWorkflowService = () => { } } + if (useCanvasStore().linearMode) { + app.rootGraph.extra ??= {} + app.rootGraph.extra.linearMode = true + } + const tempWorkflow = workflowStore.createNewTemporary( path ? appendJsonExt(path) : undefined, workflowData diff --git a/src/platform/workflow/management/stores/workflowStore.ts b/src/platform/workflow/management/stores/workflowStore.ts index 6f47c8445..7a3df1942 100644 --- a/src/platform/workflow/management/stores/workflowStore.ts +++ b/src/platform/workflow/management/stores/workflowStore.ts @@ -13,7 +13,6 @@ import type { ComfyWorkflowJSON, NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' -import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail' import { api } from '@/scripts/api' import { app as comfyApp } from '@/scripts/app' @@ -334,7 +333,6 @@ export const useWorkflowStore = defineStore('workflow', () => { tabActivationHistory.value.shift() } - useCanvasStore().linearMode = !!loadedWorkflow.activeState.extra?.linearMode return loadedWorkflow } diff --git a/src/platform/workflow/templates/composables/useTemplateWorkflows.ts b/src/platform/workflow/templates/composables/useTemplateWorkflows.ts index d3bea7fb9..a5ee9c3b9 100644 --- a/src/platform/workflow/templates/composables/useTemplateWorkflows.ts +++ b/src/platform/workflow/templates/composables/useTemplateWorkflows.ts @@ -121,28 +121,7 @@ export function useTemplateWorkflows() { if (!template || !template.sourceModule) return false // Use the stored source module for loading - const actualSourceModule = template.sourceModule - json = await fetchTemplateJson(id, actualSourceModule) - - // Use source module for name - const workflowName = - actualSourceModule === 'default' - ? t(`templateWorkflows.template.${id}`, id) - : id - - if (isCloud) { - useTelemetry()?.trackTemplate({ - workflow_name: id, - template_source: actualSourceModule - }) - } - - dialogStore.closeDialog() - await app.loadGraphData(json, true, true, workflowName, { - openSource: 'template' - }) - - return true + sourceModule = template.sourceModule } // Regular case for normal categories diff --git a/src/renderer/extensions/linearMode/DropZone.vue b/src/renderer/extensions/linearMode/DropZone.vue new file mode 100644 index 000000000..d83ea9129 --- /dev/null +++ b/src/renderer/extensions/linearMode/DropZone.vue @@ -0,0 +1,52 @@ + + diff --git a/src/renderer/extensions/linearMode/ImagePreview.vue b/src/renderer/extensions/linearMode/ImagePreview.vue new file mode 100644 index 000000000..e13dba7ce --- /dev/null +++ b/src/renderer/extensions/linearMode/ImagePreview.vue @@ -0,0 +1,44 @@ + + diff --git a/src/renderer/extensions/linearMode/LinearControls.vue b/src/renderer/extensions/linearMode/LinearControls.vue new file mode 100644 index 000000000..36e3514e6 --- /dev/null +++ b/src/renderer/extensions/linearMode/LinearControls.vue @@ -0,0 +1,284 @@ + + diff --git a/src/renderer/extensions/linearMode/LinearPreview.vue b/src/renderer/extensions/linearMode/LinearPreview.vue new file mode 100644 index 000000000..2e466b80e --- /dev/null +++ b/src/renderer/extensions/linearMode/LinearPreview.vue @@ -0,0 +1,184 @@ + + diff --git a/src/renderer/extensions/linearMode/OutputHistory.vue b/src/renderer/extensions/linearMode/OutputHistory.vue new file mode 100644 index 000000000..2e3bd1a5e --- /dev/null +++ b/src/renderer/extensions/linearMode/OutputHistory.vue @@ -0,0 +1,309 @@ + + diff --git a/src/renderer/extensions/linearMode/VideoPreview.vue b/src/renderer/extensions/linearMode/VideoPreview.vue new file mode 100644 index 000000000..57504fc25 --- /dev/null +++ b/src/renderer/extensions/linearMode/VideoPreview.vue @@ -0,0 +1,27 @@ + + diff --git a/src/renderer/extensions/linearMode/mediaTypes.ts b/src/renderer/extensions/linearMode/mediaTypes.ts new file mode 100644 index 000000000..1f77b34f5 --- /dev/null +++ b/src/renderer/extensions/linearMode/mediaTypes.ts @@ -0,0 +1,33 @@ +import { t } from '@/i18n' + +import type { ResultItemImpl } from '@/stores/queueStore' + +export type StatItem = { content?: string; iconClass?: string } +export const mediaTypes: Record = { + '3d': { + content: t('sideToolbar.mediaAssets.filter3D'), + iconClass: 'icon-[lucide--box]' + }, + audio: { + content: t('sideToolbar.mediaAssets.filterAudio'), + iconClass: 'icon-[lucide--audio-lines]' + }, + images: { + content: t('sideToolbar.mediaAssets.filterImage'), + iconClass: 'icon-[lucide--image]' + }, + text: { + content: t('sideToolbar.mediaAssets.filterText'), + iconClass: 'icon-[lucide--text]' + }, + video: { + content: t('sideToolbar.mediaAssets.filterVideo'), + iconClass: 'icon-[lucide--video]' + } +} + +export function getMediaType(output?: ResultItemImpl) { + if (!output) return '' + if (output.isVideo) return 'video' + return output.mediaType +} diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 8f27c189e..9f21ddaa4 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -40,7 +40,7 @@ transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`, zIndex: zIndex, opacity: nodeOpacity, - '--component-node-background': nodeBodyBackgroundColor + '--component-node-background': applyLightThemeColor(nodeData.bgcolor) } ]" v-bind="remainingPointerHandlers" @@ -168,7 +168,6 @@ import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeS import { app } from '@/scripts/app' import { useExecutionStore } from '@/stores/executionStore' import { useNodeOutputStore } from '@/stores/imagePreviewStore' -import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { isTransparent } from '@/utils/colorUtil' import { getLocatorIdFromNodeData, @@ -228,19 +227,6 @@ const bypassed = computed( ) const muted = computed((): boolean => nodeData.mode === LGraphEventMode.NEVER) -const nodeBodyBackgroundColor = computed(() => { - const colorPaletteStore = useColorPaletteStore() - - if (!nodeData.bgcolor) { - return '' - } - - return applyLightThemeColor( - nodeData.bgcolor, - Boolean(colorPaletteStore.completedActivePalette.light_theme) - ) -}) - const nodeOpacity = computed(() => { const globalOpacity = useSettingStore().get('Comfy.Node.Opacity') ?? 1 diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue index fe99c5d62..0f24cf873 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.vue +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue @@ -11,7 +11,10 @@ headerShapeClass ) " - :style="headerStyle" + :style="{ + backgroundColor: applyLightThemeColor(nodeData?.color), + opacity: useSettingStore().get('Comfy.Node.Opacity') ?? 1 + }" :data-testid="`node-header-${nodeData?.id || ''}`" @dblclick="handleDoubleClick" > @@ -104,7 +107,6 @@ import NodeBadge from '@/renderer/extensions/vueNodes/components/NodeBadge.vue' import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips' import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils' import { app } from '@/scripts/app' -import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { normalizeI18nKey } from '@/utils/formatUtil' import { getLocatorIdFromNodeData, @@ -156,23 +158,6 @@ const enterSubgraphTooltipConfig = computed(() => { return createTooltipConfig(st('enterSubgraph', 'Enter Subgraph')) }) -const headerStyle = computed(() => { - const colorPaletteStore = useColorPaletteStore() - - const opacity = useSettingStore().get('Comfy.Node.Opacity') ?? 1 - - if (!nodeData?.color) { - return { backgroundColor: '', opacity } - } - - const headerColor = applyLightThemeColor( - nodeData.color, - Boolean(colorPaletteStore.completedActivePalette.light_theme) - ) - - return { backgroundColor: headerColor, opacity } -}) - const resolveTitle = (info: VueNodeData | undefined) => { const title = (info?.title ?? '').trim() if (title.length > 0) return title diff --git a/src/renderer/extensions/vueNodes/utils/nodeStyleUtils.ts b/src/renderer/extensions/vueNodes/utils/nodeStyleUtils.ts index 143684d13..57a0b9717 100644 --- a/src/renderer/extensions/vueNodes/utils/nodeStyleUtils.ts +++ b/src/renderer/extensions/vueNodes/utils/nodeStyleUtils.ts @@ -1,14 +1,13 @@ +import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { adjustColor } from '@/utils/colorUtil' /** * Applies light theme color adjustments to a color */ -export function applyLightThemeColor( - color: string, - isLightTheme: boolean -): string { - if (!color || !isLightTheme) { - return color - } +export function applyLightThemeColor(color?: string): string { + if (!color) return '' + + if (!useColorPaletteStore().completedActivePalette.light_theme) return color + return adjustColor(color, { lightness: 0.5 }) } diff --git a/src/scripts/app.ts b/src/scripts/app.ts index edcc64517..258214dd6 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -1224,6 +1224,8 @@ export class ComfyApp { // Fit view if no nodes visible in restored viewport this.canvas.ds.computeVisibleArea(this.canvas.viewport) if ( + this.canvas.visible_area.width && + this.canvas.visible_area.height && !anyItemOverlapsRect( this.rootGraph._nodes, this.canvas.visible_area diff --git a/src/stores/imagePreviewStore.ts b/src/stores/imagePreviewStore.ts index ee8ae759b..346011d45 100644 --- a/src/stores/imagePreviewStore.ts +++ b/src/stores/imagePreviewStore.ts @@ -40,7 +40,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { const { nodeIdToNodeLocatorId, nodeToNodeLocatorId } = useWorkflowStore() const { executionIdToNodeLocatorId } = useExecutionStore() const scheduledRevoke: Record void }> = {} - const latestOutput = ref([]) + const latestPreview = ref([]) function scheduleRevoke(locator: NodeLocatorId, cb: () => void) { scheduledRevoke[locator]?.stop() @@ -147,13 +147,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { } } - //TODO:Preview params and deduplication - latestOutput.value = - (outputs as ExecutedWsMessage['output'])?.images?.map((image) => { - const imgUrlPart = new URLSearchParams(image) - const rand = app.getRandParam() - return api.apiURL(`/view?${imgUrlPart}${rand}`) - }) ?? [] app.nodeOutputs[nodeLocatorId] = outputs nodeOutputs.value[nodeLocatorId] = outputs } @@ -221,7 +214,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { scheduledRevoke[nodeLocatorId].stop() delete scheduledRevoke[nodeLocatorId] } - latestOutput.value = previewImages + latestPreview.value = previewImages app.nodePreviewImages[nodeLocatorId] = previewImages nodePreviewImages.value[nodeLocatorId] = previewImages } @@ -391,6 +384,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { // State nodeOutputs, nodePreviewImages, - latestOutput + latestPreview } }) diff --git a/src/utils/dateTimeUtil.ts b/src/utils/dateTimeUtil.ts index 13c739b0d..d72bd6101 100644 --- a/src/utils/dateTimeUtil.ts +++ b/src/utils/dateTimeUtil.ts @@ -75,3 +75,17 @@ export const formatClockTime = (ts: number, locale: string): string => { second: '2-digit' }).format(d) } + +export function formatDuration(durationSeconds?: number) { + if (durationSeconds == undefined) return '' + const hours = (durationSeconds / 60 ** 2) | 0 + const minutes = ((durationSeconds % 60 ** 2) / 60) | 0 + const seconds = (durationSeconds % 60) | 0 + const parts = [] + + if (hours > 0) parts.push(`${hours}h`) + if (minutes > 0) parts.push(`${minutes}m`) + if (seconds > 0) parts.push(`${seconds}s`) + + return parts.join(' ') +} diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index e60df5692..6dc309c1a 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -215,7 +215,7 @@ const onStatus = async (e: CustomEvent) => { await queueStore.update() // Only update assets if the assets sidebar is currently open // When sidebar is closed, AssetsSidebarTab.vue will refresh on mount - if (sidebarTabStore.activeSidebarTabId === 'assets') { + if (sidebarTabStore.activeSidebarTabId === 'assets' || linearMode.value) { await assetsStore.updateHistory() } } diff --git a/src/views/LinearView.vue b/src/views/LinearView.vue index 4ce74a4d0..c8c970620 100644 --- a/src/views/LinearView.vue +++ b/src/views/LinearView.vue @@ -1,107 +1,47 @@