mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
App mode - App mode toolbar - 3 (#9025)
## Summary Adds a new toolbar for app mode ## Changes - **What**: Adds new toolbar with builder mode disabled ## Screenshots (if applicable) <img width="172" height="220" alt="image" src="https://github.com/user-attachments/assets/16f3cf61-bcbe-4b4d-a169-01c934140354" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9025-App-mode-App-mode-toolbar-3-30d6d73d365081549af6fdd1937b098f) by [Unito](https://www.unito.io)
This commit is contained in:
126
src/components/appMode/AppModeToolbar.vue
Normal file
126
src/components/appMode/AppModeToolbar.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||||
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
import { useAppModeStore } from '@/stores/appModeStore'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const commandStore = useCommandStore()
|
||||||
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
const appModeStore = useAppModeStore()
|
||||||
|
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
|
||||||
|
|
||||||
|
const isAssetsActive = computed(
|
||||||
|
() => workspaceStore.sidebarTab.activeSidebarTab?.id === 'assets'
|
||||||
|
)
|
||||||
|
const isWorkflowsActive = computed(
|
||||||
|
() => workspaceStore.sidebarTab.activeSidebarTab?.id === 'workflows'
|
||||||
|
)
|
||||||
|
|
||||||
|
function enterBuilderMode() {
|
||||||
|
appModeStore.setMode('builder:select')
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAssets() {
|
||||||
|
void commandStore.execute('Workspace.ToggleSidebarTab.assets')
|
||||||
|
}
|
||||||
|
|
||||||
|
function showApps() {
|
||||||
|
void commandStore.execute('Workspace.ToggleSidebarTab.workflows')
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTemplates() {
|
||||||
|
useWorkflowTemplateSelectorDialog().show('sidebar')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 pointer-events-auto">
|
||||||
|
<WorkflowActionsDropdown source="app_mode_toolbar">
|
||||||
|
<template #button>
|
||||||
|
<Button
|
||||||
|
v-tooltip.right="{
|
||||||
|
value: t('sideToolbar.labels.menu'),
|
||||||
|
...tooltipOptions
|
||||||
|
}"
|
||||||
|
variant="secondary"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('sideToolbar.labels.menu')"
|
||||||
|
class="h-10 rounded-lg pl-3 pr-2 gap-1 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--panels-top-left] size-4" />
|
||||||
|
<i class="icon-[lucide--chevron-down] size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</WorkflowActionsDropdown>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
v-if="appModeStore.enableAppBuilder"
|
||||||
|
v-tooltip.right="{
|
||||||
|
value: t('linearMode.appModeToolbar.appBuilder'),
|
||||||
|
...tooltipOptions
|
||||||
|
}"
|
||||||
|
variant="secondary"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('linearMode.appModeToolbar.appBuilder')"
|
||||||
|
class="size-10 rounded-lg"
|
||||||
|
@click="enterBuilderMode"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--hammer] size-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-10 rounded-lg bg-secondary-background overflow-hidden"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-tooltip.right="{
|
||||||
|
value: t('sideToolbar.mediaAssets.title'),
|
||||||
|
...tooltipOptions
|
||||||
|
}"
|
||||||
|
variant="textonly"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('sideToolbar.mediaAssets.title')"
|
||||||
|
:class="
|
||||||
|
cn('size-10', isAssetsActive && 'bg-secondary-background-hover')
|
||||||
|
"
|
||||||
|
@click="openAssets"
|
||||||
|
>
|
||||||
|
<i class="icon-[comfy--image-ai-edit] size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-tooltip.right="{
|
||||||
|
value: t('linearMode.appModeToolbar.apps'),
|
||||||
|
...tooltipOptions
|
||||||
|
}"
|
||||||
|
variant="textonly"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('linearMode.appModeToolbar.apps')"
|
||||||
|
:class="
|
||||||
|
cn('size-10', isWorkflowsActive && 'bg-secondary-background-hover')
|
||||||
|
"
|
||||||
|
@click="showApps"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--panels-top-left] size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-tooltip.right="{
|
||||||
|
value: t('sideToolbar.templates'),
|
||||||
|
...tooltipOptions
|
||||||
|
}"
|
||||||
|
variant="textonly"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('sideToolbar.templates')"
|
||||||
|
class="size-10"
|
||||||
|
@click="openTemplates"
|
||||||
|
>
|
||||||
|
<i class="icon-[comfy--template] size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -2946,6 +2946,10 @@
|
|||||||
"layout": "On the left, you'll see your generated images, videos, and outputs. On the right, just the controls you need. Everything complex stays out of sight.",
|
"layout": "On the left, you'll see your generated images, videos, and outputs. On the right, just the controls you need. Everything complex stays out of sight.",
|
||||||
"sharing": "Sharing is easy: create your workflow, open App Mode, right-click the tab, and export. When others open your file, it launches straight into this clean view. You can share powerful workflows as simple tools without anyone needing to understand node graphs.",
|
"sharing": "Sharing is easy: create your workflow, open App Mode, right-click the tab, and export. When others open your file, it launches straight into this clean view. You can share powerful workflows as simple tools without anyone needing to understand node graphs.",
|
||||||
"widget": "If you want to control which settings appear, convert your top-level nodes into a subgraph, then use widget promotion in the toolbox above it to choose what's exposed."
|
"widget": "If you want to control which settings appear, convert your top-level nodes into a subgraph, then use widget promotion in the toolbox above it to choose what's exposed."
|
||||||
|
},
|
||||||
|
"appModeToolbar": {
|
||||||
|
"appBuilder": "App builder",
|
||||||
|
"apps": "Apps"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"missingNodes": {
|
"missingNodes": {
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ const appModeStore = useAppModeStore()
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
toastTo?: string | HTMLElement
|
toastTo?: string | HTMLElement
|
||||||
notesTo?: string | HTMLElement
|
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -194,11 +193,10 @@ defineExpose({ runButtonClick })
|
|||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<Popover
|
<Popover
|
||||||
v-if="partitionedNodes[0].length"
|
v-if="partitionedNodes[0].length"
|
||||||
align="start"
|
align="end"
|
||||||
class="overflow-y-auto overflow-x-clip max-h-(--reka-popover-content-available-height) z-100"
|
class="overflow-y-auto overflow-x-clip max-h-(--reka-popover-content-available-height) z-100"
|
||||||
:reference="notesTo"
|
side="bottom"
|
||||||
side="left"
|
:side-offset="-8"
|
||||||
:to="notesTo"
|
|
||||||
>
|
>
|
||||||
<template #button>
|
<template #button>
|
||||||
<Button variant="muted-textonly">
|
<Button variant="muted-textonly">
|
||||||
@@ -312,9 +310,4 @@ defineExpose({ runButtonClick })
|
|||||||
<span v-text="t('queue.jobAddedToQueue')" />
|
<span v-text="t('queue.jobAddedToQueue')" />
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport v-if="false" defer :to="notesTo">
|
|
||||||
<div
|
|
||||||
class="bg-base-background text-muted-foreground flex flex-col w-90 gap-2 rounded-2xl border-1 border-border-subtle py-3"
|
|
||||||
></div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
|||||||
const mode = ref<AppMode>('graph')
|
const mode = ref<AppMode>('graph')
|
||||||
const builderSaving = ref(false)
|
const builderSaving = ref(false)
|
||||||
const hasOutputs = ref(true)
|
const hasOutputs = ref(true)
|
||||||
|
const enableAppBuilder = ref(false)
|
||||||
|
|
||||||
const isBuilderMode = computed(
|
const isBuilderMode = computed(
|
||||||
() => mode.value === 'builder:select' || mode.value === 'builder:arrange'
|
() => mode.value === 'builder:select' || mode.value === 'builder:arrange'
|
||||||
@@ -23,6 +24,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
mode: readonly(mode),
|
mode: readonly(mode),
|
||||||
|
enableAppBuilder: readonly(enableAppBuilder),
|
||||||
isBuilderMode,
|
isBuilderMode,
|
||||||
isAppMode,
|
isAppMode,
|
||||||
isGraphMode,
|
isGraphMode,
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ import SplitterPanel from 'primevue/splitterpanel'
|
|||||||
import { computed, ref, useTemplateRef } from 'vue'
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import AppModeToolbar from '@/components/appMode/AppModeToolbar.vue'
|
||||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||||
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
|
||||||
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
||||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
|
||||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||||
import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue'
|
import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue'
|
||||||
@@ -25,15 +24,26 @@ import MobileMenu from '@/renderer/extensions/linearMode/MobileMenu.vue'
|
|||||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
|
import { useAppModeStore } from '@/stores/appModeStore'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const nodeOutputStore = useNodeOutputStore()
|
const nodeOutputStore = useNodeOutputStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
const appModeStore = useAppModeStore()
|
||||||
|
|
||||||
const mobileDisplay = useBreakpoints(breakpointsTailwind).smaller('md')
|
const mobileDisplay = useBreakpoints(breakpointsTailwind).smaller('md')
|
||||||
|
|
||||||
const activeTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
const activeTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
||||||
|
const sidebarOnLeft = computed(
|
||||||
|
() => settingStore.get('Comfy.Sidebar.Location') === 'left'
|
||||||
|
)
|
||||||
|
const hasLeftPanel = computed(
|
||||||
|
() => (sidebarOnLeft.value && activeTab.value) || !sidebarOnLeft.value
|
||||||
|
)
|
||||||
|
const hasRightPanel = computed(
|
||||||
|
() => sidebarOnLeft.value || (!sidebarOnLeft.value && activeTab.value)
|
||||||
|
)
|
||||||
|
|
||||||
const hasPreview = ref(false)
|
const hasPreview = ref(false)
|
||||||
whenever(
|
whenever(
|
||||||
@@ -45,8 +55,6 @@ const selectedItem = ref<AssetItem>()
|
|||||||
const selectedOutput = ref<ResultItemImpl>()
|
const selectedOutput = ref<ResultItemImpl>()
|
||||||
const canShowPreview = ref(true)
|
const canShowPreview = ref(true)
|
||||||
|
|
||||||
const topLeftRef = useTemplateRef('topLeftRef')
|
|
||||||
const topRightRef = useTemplateRef('topRightRef')
|
|
||||||
const bottomLeftRef = useTemplateRef('bottomLeftRef')
|
const bottomLeftRef = useTemplateRef('bottomLeftRef')
|
||||||
const bottomRightRef = useTemplateRef('bottomRightRef')
|
const bottomRightRef = useTemplateRef('bottomRightRef')
|
||||||
const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||||
@@ -93,28 +101,27 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
|||||||
@resizestart="({ originalEvent }) => originalEvent.preventDefault()"
|
@resizestart="({ originalEvent }) => originalEvent.preventDefault()"
|
||||||
>
|
>
|
||||||
<SplitterPanel
|
<SplitterPanel
|
||||||
|
v-if="hasLeftPanel"
|
||||||
id="linearLeftPanel"
|
id="linearLeftPanel"
|
||||||
:size="1"
|
:size="1"
|
||||||
class="min-w-min outline-none"
|
class="min-w-min outline-none"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="settingStore.get('Comfy.Sidebar.Location') === 'left'"
|
v-if="sidebarOnLeft && activeTab"
|
||||||
class="flex h-full border-border-subtle border-r"
|
class="flex h-full border-border-subtle border-r"
|
||||||
>
|
>
|
||||||
<SideToolbar />
|
<ExtensionSlot :extension="activeTab" />
|
||||||
<ExtensionSlot v-if="activeTab" :extension="activeTab" />
|
|
||||||
</div>
|
</div>
|
||||||
<LinearControls
|
<LinearControls
|
||||||
v-else
|
v-else
|
||||||
ref="linearWorkflowRef"
|
ref="linearWorkflowRef"
|
||||||
:toast-to="unrefElement(bottomLeftRef) ?? undefined"
|
:toast-to="unrefElement(bottomLeftRef) ?? undefined"
|
||||||
:notes-to="unrefElement(topLeftRef) ?? undefined"
|
|
||||||
/>
|
/>
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
<SplitterPanel
|
<SplitterPanel
|
||||||
id="linearCenterPanel"
|
id="linearCenterPanel"
|
||||||
:size="98"
|
: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"
|
class="flex flex-col min-w-min gap-4 px-10 pt-8 pb-4 relative text-muted-foreground outline-none"
|
||||||
>
|
>
|
||||||
<LinearPreview
|
<LinearPreview
|
||||||
:latent-preview="
|
:latent-preview="
|
||||||
@@ -126,10 +133,9 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
|||||||
:selected-item
|
:selected-item
|
||||||
:selected-output
|
:selected-output
|
||||||
/>
|
/>
|
||||||
<div ref="topLeftRef" class="absolute z-21 top-4 left-4">
|
<div class="absolute z-21 top-1 left-1">
|
||||||
<WorkflowActionsDropdown source="app_mode_menu_selected" />
|
<AppModeToolbar v-if="!appModeStore.isBuilderMode" />
|
||||||
</div>
|
</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="bottomLeftRef" class="absolute z-20 bottom-4 left-4" />
|
||||||
<div ref="bottomRightRef" class="absolute z-20 bottom-24 right-4" />
|
<div ref="bottomRightRef" class="absolute z-20 bottom-24 right-4" />
|
||||||
<div
|
<div
|
||||||
@@ -146,19 +152,21 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
|||||||
</div>
|
</div>
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
<SplitterPanel
|
<SplitterPanel
|
||||||
|
v-if="hasRightPanel"
|
||||||
id="linearRightPanel"
|
id="linearRightPanel"
|
||||||
:size="1"
|
:size="1"
|
||||||
class="min-w-min outline-none"
|
class="min-w-min outline-none"
|
||||||
>
|
>
|
||||||
<LinearControls
|
<LinearControls
|
||||||
v-if="settingStore.get('Comfy.Sidebar.Location') === 'left'"
|
v-if="sidebarOnLeft"
|
||||||
ref="linearWorkflowRef"
|
ref="linearWorkflowRef"
|
||||||
:toast-to="unrefElement(bottomRightRef) ?? undefined"
|
:toast-to="unrefElement(bottomRightRef) ?? undefined"
|
||||||
:notes-to="unrefElement(topRightRef) ?? undefined"
|
|
||||||
/>
|
/>
|
||||||
<div v-else class="flex h-full border-border-subtle border-l">
|
<div
|
||||||
<ExtensionSlot v-if="activeTab" :extension="activeTab" />
|
v-else-if="activeTab"
|
||||||
<SideToolbar class="border-border-subtle border-l" />
|
class="flex h-full border-border-subtle border-l"
|
||||||
|
>
|
||||||
|
<ExtensionSlot :extension="activeTab" />
|
||||||
</div>
|
</div>
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
</Splitter>
|
</Splitter>
|
||||||
|
|||||||
Reference in New Issue
Block a user