mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
Sidebar tab API for extensions (#215)
* Add extensionManager to manage tabs * Fix null bug * nit
This commit is contained in:
20
src/App.vue
20
src/App.vue
@@ -4,8 +4,8 @@
|
||||
<NodeSearchboxPopover v-if="nodeSearchEnabled" />
|
||||
<teleport to="#graph-canvas-container">
|
||||
<LiteGraphCanvasSplitterOverlay>
|
||||
<template #side-bar-panel="{ setPanelVisible }">
|
||||
<SideToolBar @change="setPanelVisible($event)" />
|
||||
<template #side-bar-panel>
|
||||
<SideToolBar />
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
</teleport>
|
||||
@@ -13,14 +13,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { computed, markRaw, onMounted, ref, watch } from "vue";
|
||||
import NodeSearchboxPopover from "@/components/NodeSearchBoxPopover.vue";
|
||||
import SideToolBar from "@/components/sidebar/SideToolBar.vue";
|
||||
import LiteGraphCanvasSplitterOverlay from "@/components/LiteGraphCanvasSplitterOverlay.vue";
|
||||
import QueueSideBarTab from "@/components/sidebar/tabs/QueueSideBarTab.vue";
|
||||
import ProgressSpinner from "primevue/progressspinner";
|
||||
import { app } from "./scripts/app";
|
||||
import { useSettingStore } from "./stores/settingStore";
|
||||
import { useNodeDefStore } from "./stores/nodeDefStore";
|
||||
import { ExtensionManagerImpl } from "./scripts/extensionManager";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const isLoading = ref(true);
|
||||
const nodeSearchEnabled = computed<boolean>(
|
||||
@@ -43,10 +46,21 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
const init = () => {
|
||||
useNodeDefStore().addNodeDefs(Object.values(app.nodeDefs));
|
||||
useSettingStore().addSettings(app.ui.settings);
|
||||
app.vueAppReady = true;
|
||||
// Late init as extension manager needs to access pinia store.
|
||||
app.extensionManager = new ExtensionManagerImpl();
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "queue",
|
||||
icon: "pi pi-history",
|
||||
title: t("sideToolBar.queue"),
|
||||
tooltip: t("sideToolBar.queue"),
|
||||
component: markRaw(QueueSideBarTab),
|
||||
type: "vue",
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:size="20"
|
||||
v-show="sideBarPanelVisible"
|
||||
>
|
||||
<slot name="side-bar-panel" :setPanelVisible="setPanelVisible"></slot>
|
||||
<slot name="side-bar-panel"></slot>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel class="graph-canvas-panel" :size="100">
|
||||
<div></div>
|
||||
@@ -15,14 +15,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWorkspaceStore } from "@/stores/workspaceStateStore";
|
||||
import Splitter from "primevue/splitter";
|
||||
import SplitterPanel from "primevue/splitterpanel";
|
||||
import { computed, ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const sideBarPanelVisible = ref(false);
|
||||
const setPanelVisible = (visible: boolean) => {
|
||||
sideBarPanelVisible.value = visible;
|
||||
};
|
||||
const sideBarPanelVisible = computed(
|
||||
() => useWorkspaceStore().activeSidebarTab !== null
|
||||
);
|
||||
const gutterClass = computed(() => {
|
||||
return sideBarPanelVisible.value ? "" : "gutter-hidden";
|
||||
});
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
<teleport to=".comfyui-body-left">
|
||||
<nav class="side-tool-bar-container">
|
||||
<SideBarIcon
|
||||
v-for="item in items"
|
||||
:icon="item.icon"
|
||||
:tooltip="item.tooltip"
|
||||
:selected="item === selectedItem"
|
||||
@click="onItemClick(item)"
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:icon="tab.icon"
|
||||
:tooltip="tab.tooltip"
|
||||
:selected="tab === selectedTab"
|
||||
@click="onTabClick(tab)"
|
||||
/>
|
||||
<div class="side-tool-bar-end">
|
||||
<SideBarThemeToggleIcon />
|
||||
@@ -14,50 +15,68 @@
|
||||
</div>
|
||||
</nav>
|
||||
</teleport>
|
||||
<component :is="selectedItem?.component" />
|
||||
<div v-if="!selectedTab"></div>
|
||||
<component
|
||||
v-else-if="selectedTab.type === 'vue'"
|
||||
:is="selectedTab.component"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el)
|
||||
mountCustomTab(
|
||||
selectedTab as CustomSidebarTabExtension,
|
||||
el as HTMLElement
|
||||
);
|
||||
}
|
||||
"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SideBarIcon from "./SideBarIcon.vue";
|
||||
import SideBarThemeToggleIcon from "./SideBarThemeToggleIcon.vue";
|
||||
import SideBarSettingsToggleIcon from "./SideBarSettingsToggleIcon.vue";
|
||||
import NodeDetailSideBarItem from "./items/NodeDetailSideBarItem.vue";
|
||||
import QueueSideBarItem from "./items/QueueSideBarItem.vue";
|
||||
import { computed, markRaw, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, onBeforeUnmount, watch } from "vue";
|
||||
import { useSettingStore } from "@/stores/settingStore";
|
||||
import { app } from "@/scripts/app";
|
||||
import { useWorkspaceStore } from "@/stores/workspaceStateStore";
|
||||
import {
|
||||
CustomSidebarTabExtension,
|
||||
SidebarTabExtension,
|
||||
} from "@/types/extensionTypes";
|
||||
|
||||
const { t } = useI18n();
|
||||
const items = ref([
|
||||
// { icon: "pi pi-map", component: markRaw(NodeDetailSideBarItem) },
|
||||
{
|
||||
icon: "pi pi-history",
|
||||
tooltip: t("sideToolBar.queue"),
|
||||
component: markRaw(QueueSideBarItem),
|
||||
},
|
||||
]);
|
||||
const selectedItem = ref(null);
|
||||
const onItemClick = (item) => {
|
||||
if (selectedItem.value === item) {
|
||||
selectedItem.value = null;
|
||||
return;
|
||||
}
|
||||
selectedItem.value = item;
|
||||
};
|
||||
|
||||
const emit = defineEmits(["change"]);
|
||||
watch(selectedItem, (newVal) => {
|
||||
emit("change", newVal !== null);
|
||||
const workspaceStateStore = useWorkspaceStore();
|
||||
const tabs = computed(() => app.extensionManager.getSidebarTabs());
|
||||
const selectedTab = computed<SidebarTabExtension | null>(() => {
|
||||
const tabId = workspaceStateStore.activeSidebarTab;
|
||||
return tabs.value.find((tab) => tab.id === tabId) || null;
|
||||
});
|
||||
const mountCustomTab = (tab: CustomSidebarTabExtension, el: HTMLElement) => {
|
||||
tab.render(el);
|
||||
};
|
||||
const onTabClick = (item: SidebarTabExtension) => {
|
||||
workspaceStateStore.updateActiveSidebarTab(
|
||||
workspaceStateStore.activeSidebarTab === item.id ? null : item.id
|
||||
);
|
||||
};
|
||||
|
||||
const betaMenuEnabled = computed(
|
||||
() => useSettingStore().get("Comfy.UseNewMenu") !== "Disabled"
|
||||
);
|
||||
watch(betaMenuEnabled, (newValue) => {
|
||||
if (!newValue) {
|
||||
selectedItem.value = null;
|
||||
workspaceStateStore.updateActiveSidebarTab(null);
|
||||
}
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
tabs.value.forEach((tab) => {
|
||||
if (tab.type === "custom" && tab.destroy) {
|
||||
tab.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<Accordion>
|
||||
<AccordionPanel v-for="node in nodes" :key="node.id">
|
||||
<AccordionHeader>
|
||||
<div>{{ node.id }} - {{ node.type }}</div>
|
||||
</AccordionHeader>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defaultGraph } from "@/scripts/defaultGraph";
|
||||
import Accordion from "primevue/accordion";
|
||||
import AccordionHeader from "primevue/accordionheader";
|
||||
import AccordionPanel from "primevue/accordionpanel";
|
||||
|
||||
/* Placeholder workflow display */
|
||||
const workflow = defaultGraph;
|
||||
const nodes = workflow.nodes.sort((a, b) => a.id - b.id);
|
||||
</script>
|
||||
@@ -35,6 +35,7 @@ import { StorageLocation } from "@/types/settingTypes";
|
||||
// CSS imports. style.css must be imported later as it overwrites some litegraph styles.
|
||||
import "@comfyorg/litegraph/css/litegraph.css";
|
||||
import "../assets/css/style.css";
|
||||
import { ExtensionManager } from "@/types/extensionTypes";
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview";
|
||||
|
||||
@@ -90,6 +91,7 @@ export class ComfyApp {
|
||||
ui: ComfyUI;
|
||||
logging: ComfyLogging;
|
||||
extensions: ComfyExtension[];
|
||||
extensionManager: ExtensionManager;
|
||||
_nodeOutputs: Record<string, any>;
|
||||
nodePreviewImages: Record<string, typeof Image>;
|
||||
shiftDown: boolean;
|
||||
|
||||
43
src/scripts/extensionManager.ts
Normal file
43
src/scripts/extensionManager.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useWorkspaceStore } from "@/stores/workspaceStateStore";
|
||||
import { ExtensionManager, SidebarTabExtension } from "@/types/extensionTypes";
|
||||
|
||||
export class ExtensionManagerImpl implements ExtensionManager {
|
||||
private sidebarTabs: SidebarTabExtension[] = [];
|
||||
private workspaceStore = useWorkspaceStore();
|
||||
|
||||
registerSidebarTab(tab: SidebarTabExtension) {
|
||||
this.sidebarTabs.push(tab);
|
||||
this.updateSidebarOrder();
|
||||
}
|
||||
|
||||
unregisterSidebarTab(id: string) {
|
||||
const index = this.sidebarTabs.findIndex((tab) => tab.id === id);
|
||||
if (index !== -1) {
|
||||
const tab = this.sidebarTabs[index];
|
||||
if (tab.type === "custom" && tab.destroy) {
|
||||
tab.destroy();
|
||||
}
|
||||
this.sidebarTabs.splice(index, 1);
|
||||
this.updateSidebarOrder();
|
||||
}
|
||||
}
|
||||
|
||||
getSidebarTabs() {
|
||||
return this.sidebarTabs.sort((a, b) => {
|
||||
const orderA = this.workspaceStore.sidebarTabsOrder.indexOf(a.id);
|
||||
const orderB = this.workspaceStore.sidebarTabsOrder.indexOf(b.id);
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
|
||||
private updateSidebarOrder() {
|
||||
const currentOrder = this.workspaceStore.sidebarTabsOrder;
|
||||
const newTabs = this.sidebarTabs.filter(
|
||||
(tab) => !currentOrder.includes(tab.id)
|
||||
);
|
||||
this.workspaceStore.updateSidebarOrder([
|
||||
...currentOrder,
|
||||
...newTabs.map((tab) => tab.id),
|
||||
]);
|
||||
}
|
||||
}
|
||||
32
src/stores/workspaceStateStore.ts
Normal file
32
src/stores/workspaceStateStore.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
interface WorkspaceState {
|
||||
activeSidebarTab: string | null;
|
||||
sidebarTabsOrder: string[]; // Array of tab IDs in order
|
||||
}
|
||||
|
||||
export const useWorkspaceStore = defineStore("workspace", {
|
||||
state: (): WorkspaceState => ({
|
||||
activeSidebarTab: null,
|
||||
sidebarTabsOrder: [],
|
||||
}),
|
||||
actions: {
|
||||
updateActiveSidebarTab(tabId: string) {
|
||||
this.activeSidebarTab = tabId;
|
||||
},
|
||||
updateSidebarOrder(newOrder: string[]) {
|
||||
this.sidebarTabsOrder = newOrder;
|
||||
},
|
||||
serialize() {
|
||||
return JSON.stringify({
|
||||
activeSidebarTab: this.activeSidebarTab,
|
||||
sidebarTabsOrder: this.sidebarTabsOrder,
|
||||
});
|
||||
},
|
||||
deserialize(state: string) {
|
||||
const parsedState = JSON.parse(state);
|
||||
this.sidebarTabsOrder = parsedState.sidebarTabsOrder;
|
||||
this.activeSidebarTab = parsedState.activeSidebarTab;
|
||||
},
|
||||
},
|
||||
});
|
||||
30
src/types/extensionTypes.ts
Normal file
30
src/types/extensionTypes.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Component } from "vue";
|
||||
|
||||
export interface BaseSidebarTabExtension {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
order?: number;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export interface VueSidebarTabExtension extends BaseSidebarTabExtension {
|
||||
type: "vue";
|
||||
component: Component;
|
||||
}
|
||||
|
||||
export interface CustomSidebarTabExtension extends BaseSidebarTabExtension {
|
||||
type: "custom";
|
||||
render: (container: HTMLElement) => void;
|
||||
destroy?: () => void;
|
||||
}
|
||||
|
||||
export type SidebarTabExtension =
|
||||
| VueSidebarTabExtension
|
||||
| CustomSidebarTabExtension;
|
||||
|
||||
export interface ExtensionManager {
|
||||
registerSidebarTab(tab: SidebarTabExtension): void;
|
||||
unregisterSidebarTab(id: string): void;
|
||||
getSidebarTabs(): SidebarTabExtension[];
|
||||
}
|
||||
Reference in New Issue
Block a user