mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-09 01:20:09 +00:00
linear v2: Simple Mode (#7734)
A major, full rewrite of linear mode, now under the name "Simple Mode". - Fixes widget styling - Adds a new simplified history - Adds support for non-image outputs - Supports right sidebar - Allows and panning on the output image preview - Provides support for drag and drop zones - Moves workflow notes into a popover. - Allows scrolling through outputs with Ctrl+scroll or arrow keys The primary means of accessing Simple Mode is a toggle button on the bottom right. This button is only shown if a feature flag is enabled, or the user has already seen linear mode during the current session. Simple Mode can also be accessed by - Using the toggle linear mode keybind - Loading a workflow that that was saved in Simple Mode workflow - Loading a template url with appropriate parameter <img width="1790" height="1387" alt="image" src="https://github.com/user-attachments/assets/d86a4a41-dfbf-41e7-a6d9-146473005606" /> Known issues: - Outputs on cloud are not filtered to those produced by the current workflow. - Output filtering has been globally disabled for consistency - Outputs will load more items on scroll, but does not unload - Performance may be reduced on weak devices with very large histories. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7734-linear-v2-2d16d73d3650819b8a10f150ff12ea22) by [Unito](https://www.unito.io)
This commit is contained in:
28
src/components/sidebar/ModeToggle.vue
Normal file
28
src/components/sidebar/ModeToggle.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
</script>
|
||||
<template>
|
||||
<div class="p-1 bg-secondary-background rounded-lg w-10">
|
||||
<Button
|
||||
size="icon"
|
||||
:title="t('linearMode.linearMode')"
|
||||
:variant="canvasStore.linearMode ? 'inverted' : 'secondary'"
|
||||
@click="useCommandStore().execute('Comfy.ToggleLinear')"
|
||||
>
|
||||
<i class="icon-[lucide--panels-top-left]" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
:title="t('linearMode.graphMode')"
|
||||
:variant="canvasStore.linearMode ? 'secondary' : 'inverted'"
|
||||
@click="useCommandStore().execute('Comfy.ToggleLinear')"
|
||||
>
|
||||
<i class="icon-[comfy--workflow]" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -44,6 +44,7 @@
|
||||
<SidebarBottomPanelToggleButton :is-small="isSmall" />
|
||||
<SidebarShortcutsToggleButton :is-small="isSmall" />
|
||||
<SidebarSettingsButton :is-small="isSmall" />
|
||||
<ModeToggle v-if="showLinearToggle" />
|
||||
</div>
|
||||
</div>
|
||||
<HelpCenterPopups :is-small="isSmall" />
|
||||
@@ -51,15 +52,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { useResizeObserver, whenever } from '@vueuse/core'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import HelpCenterPopups from '@/components/helpcenter/HelpCenterPopups.vue'
|
||||
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
|
||||
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
||||
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
|
||||
import SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'
|
||||
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
@@ -84,6 +87,12 @@ const sideToolbarRef = ref<HTMLElement>()
|
||||
const topToolbarRef = ref<HTMLElement>()
|
||||
const bottomToolbarRef = ref<HTMLElement>()
|
||||
|
||||
const showLinearToggle = ref(useFeatureFlags().flags.linearToggleEnabled)
|
||||
whenever(
|
||||
() => canvasStore.linearMode,
|
||||
() => (showLinearToggle.value = true)
|
||||
)
|
||||
|
||||
const isSmall = computed(
|
||||
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.workflows')"
|
||||
v-bind="$attrs"
|
||||
class="workflows-sidebar-tab"
|
||||
>
|
||||
<template #tool-buttons>
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
>
|
||||
<i class="pi pi-bars" />
|
||||
</Button>
|
||||
<i
|
||||
v-else-if="workflowOption.workflow.activeState?.extra?.linearMode"
|
||||
class="icon-[lucide--panels-top-left] bg-primary-background"
|
||||
/>
|
||||
<span class="workflow-label inline-block max-w-[150px] truncate text-sm">
|
||||
{{ workflowOption.workflow.filename }}
|
||||
</span>
|
||||
|
||||
76
src/components/ui/Popover.vue
Normal file
76
src/components/ui/Popover.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
PopoverRoot,
|
||||
PopoverTrigger
|
||||
} from 'reka-ui'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
entries?: { label: string; action?: () => void; icon?: string }[][]
|
||||
icon?: string
|
||||
to?: string | HTMLElement
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverRoot v-slot="{ close }">
|
||||
<PopoverTrigger as-child>
|
||||
<slot name="button">
|
||||
<Button size="icon">
|
||||
<i :class="icon ?? 'icon-[lucide--ellipsis]'" />
|
||||
</Button>
|
||||
</slot>
|
||||
</PopoverTrigger>
|
||||
<PopoverPortal :to>
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
:side-offset="5"
|
||||
:collision-padding="10"
|
||||
v-bind="$attrs"
|
||||
class="rounded-lg p-2 bg-base-background shadow-sm border border-border-subtle will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<div
|
||||
v-for="{ label, action, icon } in entryGroup"
|
||||
:key="label"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-row gap-4 p-2 rounded-sm my-1',
|
||||
action &&
|
||||
'cursor-pointer hover:bg-secondary-background-hover'
|
||||
)
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
if (!action) return
|
||||
action()
|
||||
close()
|
||||
}
|
||||
"
|
||||
>
|
||||
<i v-if="icon" :class="icon" />
|
||||
{{ label }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</slot>
|
||||
<PopoverArrow class="fill-base-background stroke-border-subtle" />
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</PopoverRoot>
|
||||
</template>
|
||||
29
src/components/ui/TypeformPopoverButton.vue
Normal file
29
src/components/ui/TypeformPopoverButton.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
defineProps<{
|
||||
dataTfWidget: string
|
||||
}>()
|
||||
|
||||
const feedbackRef = useTemplateRef('feedbackRef')
|
||||
|
||||
whenever(feedbackRef, () => {
|
||||
const scriptEl = document.createElement('script')
|
||||
scriptEl.src = '//embed.typeform.com/next/embed.js'
|
||||
feedbackRef.value?.appendChild(scriptEl)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<Popover>
|
||||
<template #button>
|
||||
<Button variant="inverted" class="rounded-full size-12">
|
||||
<i class="icon-[lucide--circle-question-mark] size-6" />
|
||||
</Button>
|
||||
</template>
|
||||
<div ref="feedbackRef" data-tf-auto-resize :data-tf-widget />
|
||||
</Popover>
|
||||
</template>
|
||||
59
src/components/ui/ZoomPane.vue
Normal file
59
src/components/ui/ZoomPane.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
|
||||
const zoomPane = useTemplateRef('zoomPane')
|
||||
|
||||
const zoom = ref(1.0)
|
||||
const panX = ref(0.0)
|
||||
const panY = ref(0.0)
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
const zoomPaneEl = zoomPane.value
|
||||
if (!zoomPaneEl) return
|
||||
|
||||
zoom.value -= e.deltaY
|
||||
const { x, y, width, height } = zoomPaneEl.getBoundingClientRect()
|
||||
const offsetX = e.clientX - x - width / 2
|
||||
const offsetY = e.clientY - y - height / 2
|
||||
const scaler = 1.1 ** (e.deltaY / -30)
|
||||
|
||||
panY.value = panY.value * scaler - offsetY * (scaler - 1)
|
||||
panX.value = panX.value * scaler - offsetX * (scaler - 1)
|
||||
}
|
||||
|
||||
let dragging = false
|
||||
function handleDown(e: PointerEvent) {
|
||||
if (e.button !== 0) return
|
||||
|
||||
const zoomPaneEl = zoomPane.value
|
||||
if (!zoomPaneEl) return
|
||||
zoomPaneEl.parentElement?.focus()
|
||||
|
||||
zoomPaneEl.setPointerCapture(e.pointerId)
|
||||
dragging = true
|
||||
}
|
||||
function handleMove(e: PointerEvent) {
|
||||
if (!dragging) return
|
||||
panX.value += e.movementX
|
||||
panY.value += e.movementY
|
||||
}
|
||||
|
||||
const transform = computed(() => {
|
||||
const scale = 1.1 ** (zoom.value / 30)
|
||||
const matrix = [scale, 0, 0, scale, panX.value, panY.value]
|
||||
return `matrix(${matrix.join(',')})`
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
ref="zoomPane"
|
||||
class="contain-size flex place-content-center"
|
||||
@wheel="handleWheel"
|
||||
@pointerdown.prevent="handleDown"
|
||||
@pointermove="handleMove"
|
||||
@pointerup="dragging = false"
|
||||
@pointercancel="dragging = false"
|
||||
>
|
||||
<slot :style="{ transform }" />
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user