Compare commits

..

6 Commits

Author SHA1 Message Date
Terry Jia
c598d2653d remove irrelevant code 2025-09-22 15:27:38 -04:00
Terry Jia
13261b3621 add bypassLOD 2025-09-22 15:01:24 -04:00
Alexander Piskun
f086377307 add pricing for new api nodes (#5724)
## Summary

Added prices for the new upcoming API nodes. Backport required.
2025-09-22 11:33:00 -07:00
filtered
687b9e659c Fix reroute ID 0 treated as invalid (#5723)
## Summary

Fixes old logic bug from refactor
https://github.com/Comfy-Org/litegraph.js/pull/602/files

## Changes

- Fixes truthy refactor to explicitly check undefined

## Review Focus

No expectation that this will impact prod, however it may impact
extensions IF someone has explicitly been setting link parentId to 0.
This would be very strange, as it would cause unexpected behaviour in
other parts of the code (which all explicitly check `undefined`).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5723-Fix-reroute-ID-0-treated-as-invalid-2766d73d365081568124ce1f85cdf84e)
by [Unito](https://www.unito.io)
2025-09-22 11:13:38 -07:00
Christian Byrne
da0d51311b fix Vue node being dragged when interacting with widgets (e.g., resizing textarea) (#5719)
## Summary

Applying changes in
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5516 to entire widget
wrapper.
 
## Changes

- **What**: Added `.stop` modifier to pointer events in NodeWidgets
component to prevent [event
propagation](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)

## Review Focus

Verify widget interactions remain functional while ensuring parent node
drag/selection behavior is properly isolated.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5719-fix-Vue-node-being-dragged-when-interacting-with-widgets-e-g-resizing-textarea-2766d73d3650815091adcd1d65197c7b)
by [Unito](https://www.unito.io)
2025-09-21 21:56:03 -07:00
Christian Byrne
e314d9cbd9 [refactor] Simplify current user resolved hook implementation (#5718)
## Summary

Refactored `onUserResolved` function in auth composable to use VueUse
`whenever` utility instead of manual watch implementation and use
`immediate` option instead of invoking manually before creating watcher.

## Changes

- **What**: Replaced manual watch + immediate check pattern with [VueUse
whenever](https://vueuse.org/shared/whenever/) utility in
`useCurrentUser.ts:37`

## Review Focus

Behavioral equivalence verification - `whenever` with `immediate: true`
should maintain identical callback timing and cleanup semantics.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5718-refactor-Simplify-current-user-resolved-hook-implementation-2766d73d365081008b6de156dd78f940)
by [Unito](https://www.unito.io)
2025-09-21 21:53:25 -07:00
305 changed files with 10160 additions and 9941 deletions

View File

@@ -98,12 +98,6 @@ export default defineConfig([
// */
'vue/one-component-per-file': 'off', // TODO: fix
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
'vue/block-order': [
'error',
{
order: ['docs', 'script', 'template', 'i18n', 'style']
}
],
// Restrict deprecated PrimeVue components
'no-restricted-imports': [
'error',

View File

@@ -1,3 +1,13 @@
<template>
<router-view />
<ProgressSpinner
v-if="isLoading"
class="absolute inset-0 flex justify-center items-center h-[unset]"
/>
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
</template>
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import BlockUI from 'primevue/blockui'
@@ -45,13 +55,3 @@ onMounted(() => {
void conflictDetection.initializeConflictDetection()
})
</script>
<template>
<router-view />
<ProgressSpinner
v-if="isLoading"
class="absolute inset-0 flex justify-center items-center h-[unset]"
/>
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
</template>

View File

@@ -1,36 +1,3 @@
<script setup lang="ts">
import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
const settingStore = useSettingStore()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const unifiedWidth = computed(() =>
settingStore.get('Comfy.Sidebar.UnifiedWidth')
)
const sidebarPanelVisible = computed(
() => useSidebarTabStore().activeSidebarTab !== null
)
const bottomPanelVisible = computed(
() => useBottomPanelStore().bottomPanelVisible
)
const activeSidebarTabId = computed(
() => useSidebarTabStore().activeSidebarTabId
)
const sidebarStateKey = computed(() => {
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
})
</script>
<template>
<Splitter
:key="sidebarStateKey"
@@ -78,6 +45,39 @@ const sidebarStateKey = computed(() => {
</Splitter>
</template>
<script setup lang="ts">
import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
const settingStore = useSettingStore()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const unifiedWidth = computed(() =>
settingStore.get('Comfy.Sidebar.UnifiedWidth')
)
const sidebarPanelVisible = computed(
() => useSidebarTabStore().activeSidebarTab !== null
)
const bottomPanelVisible = computed(
() => useBottomPanelStore().bottomPanelVisible
)
const activeSidebarTabId = computed(
() => useSidebarTabStore().activeSidebarTabId
)
const sidebarStateKey = computed(() => {
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
})
</script>
<style scoped>
@reference '../assets/css/style.css';

View File

@@ -1,3 +1,24 @@
<template>
<div
v-show="workspaceState.focusMode"
class="comfy-menu-hamburger no-drag"
:style="positionCSS"
>
<Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
icon="pi pi-bars"
severity="secondary"
text
size="large"
:aria-label="$t('menu.showMenu')"
aria-live="assertive"
@click="exitFocusMode"
@contextmenu="showNativeSystemMenu"
/>
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import type { CSSProperties } from 'vue'
@@ -35,27 +56,6 @@ const positionCSS = computed<CSSProperties>(() =>
)
</script>
<template>
<div
v-show="workspaceState.focusMode"
class="comfy-menu-hamburger no-drag"
:style="positionCSS"
>
<Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
icon="pi pi-bars"
severity="secondary"
text
size="large"
:aria-label="$t('menu.showMenu')"
aria-live="assertive"
@click="exitFocusMode"
@contextmenu="showNativeSystemMenu"
/>
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
</div>
</template>
<style scoped>
@reference '../assets/css/style.css';

View File

@@ -1,34 +1,3 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import InputNumber from 'primevue/inputnumber'
import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
const queueSettingsStore = useQueueSettingsStore()
const { batchCount } = storeToRefs(queueSettingsStore)
const minQueueCount = 1
const settingStore = useSettingStore()
const maxQueueCount = computed(() =>
settingStore.get('Comfy.QueueButton.BatchCountLimit')
)
const handleClick = (increment: boolean) => {
let newCount: number
if (increment) {
const originalCount = batchCount.value - 1
newCount = Math.min(originalCount * 2, maxQueueCount.value)
} else {
const originalCount = batchCount.value + 1
newCount = Math.floor(originalCount / 2)
}
batchCount.value = newCount
}
</script>
<template>
<div
v-tooltip.bottom="{
@@ -63,6 +32,37 @@ const handleClick = (increment: boolean) => {
</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import InputNumber from 'primevue/inputnumber'
import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
const queueSettingsStore = useQueueSettingsStore()
const { batchCount } = storeToRefs(queueSettingsStore)
const minQueueCount = 1
const settingStore = useSettingStore()
const maxQueueCount = computed(() =>
settingStore.get('Comfy.QueueButton.BatchCountLimit')
)
const handleClick = (increment: boolean) => {
let newCount: number
if (increment) {
const originalCount = batchCount.value - 1
newCount = Math.min(originalCount * 2, maxQueueCount.value)
} else {
const originalCount = batchCount.value + 1
newCount = Math.floor(originalCount / 2)
}
batchCount.value = newCount
}
</script>
<style scoped>
:deep(.p-inputtext) {
border-top-left-radius: 0;

View File

@@ -1,3 +1,16 @@
<template>
<Panel
class="actionbar w-fit"
:style="style"
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
>
<div ref="panelRef" class="actionbar-content flex items-center select-none">
<span ref="dragHandleRef" class="drag-handle cursor-move mr-2" />
<ComfyQueueButton />
</div>
</Panel>
</template>
<script lang="ts" setup>
import {
useDraggable,
@@ -215,19 +228,6 @@ watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
})
</script>
<template>
<Panel
class="actionbar w-fit"
:style="style"
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
>
<div ref="panelRef" class="actionbar-content flex items-center select-none">
<span ref="dragHandleRef" class="drag-handle cursor-move mr-2" />
<ComfyQueueButton />
</div>
</Panel>
</template>
<style scoped>
@reference '../../assets/css/style.css';

View File

@@ -1,74 +1,3 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import Button from 'primevue/button'
import ButtonGroup from 'primevue/buttongroup'
import SplitButton from 'primevue/splitbutton'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import {
useQueuePendingTaskCountStore,
useQueueSettingsStore
} from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import BatchCountEdit from './BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => ({
disabled: {
key: 'disabled',
label: t('menu.run'),
tooltip: t('menu.disabledTooltip'),
command: () => {
queueMode.value = 'disabled'
}
},
instant: {
key: 'instant',
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
queueMode.value = 'instant'
}
},
change: {
key: 'change',
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
queueMode.value = 'change'
}
}
}))
const activeQueueModeMenuItem = computed(
() => queueModeMenuItemLookup.value[queueMode.value]
)
const queueModeMenuItems = computed(() =>
Object.values(queueModeMenuItemLookup.value)
)
const executingPrompt = computed(() => !!queueCountStore.count.value)
const hasPendingTasks = computed(
() => queueCountStore.count.value > 1 || queueMode.value !== 'disabled'
)
const commandStore = useCommandStore()
const queuePrompt = async (e: Event) => {
const commandId =
'shiftKey' in e && e.shiftKey
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
await commandStore.execute(commandId)
}
</script>
<template>
<div class="queue-button-group flex">
<SplitButton
@@ -143,6 +72,77 @@ const queuePrompt = async (e: Event) => {
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import Button from 'primevue/button'
import ButtonGroup from 'primevue/buttongroup'
import SplitButton from 'primevue/splitbutton'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import {
useQueuePendingTaskCountStore,
useQueueSettingsStore
} from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import BatchCountEdit from './BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => ({
disabled: {
key: 'disabled',
label: t('menu.run'),
tooltip: t('menu.disabledTooltip'),
command: () => {
queueMode.value = 'disabled'
}
},
instant: {
key: 'instant',
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
queueMode.value = 'instant'
}
},
change: {
key: 'change',
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
queueMode.value = 'change'
}
}
}))
const activeQueueModeMenuItem = computed(
() => queueModeMenuItemLookup.value[queueMode.value]
)
const queueModeMenuItems = computed(() =>
Object.values(queueModeMenuItemLookup.value)
)
const executingPrompt = computed(() => !!queueCountStore.count.value)
const hasPendingTasks = computed(
() => queueCountStore.count.value > 1 || queueMode.value !== 'disabled'
)
const commandStore = useCommandStore()
const queuePrompt = async (e: Event) => {
const commandId =
'shiftKey' in e && e.shiftKey
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
await commandStore.execute(commandId)
}
</script>
<style scoped>
.comfyui-queue-button :deep(.p-splitbutton-dropdown) {
border-top-right-radius: 0;

View File

@@ -1,46 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Tab from 'primevue/tab'
import TabList from 'primevue/tablist'
import Tabs from 'primevue/tabs'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import { useDialogService } from '@/services/dialogService'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import type { BottomPanelExtension } from '@/types/extensionTypes'
const bottomPanelStore = useBottomPanelStore()
const dialogService = useDialogService()
const { t } = useI18n()
const isShortcutsTabActive = computed(() => {
const activeTabId = bottomPanelStore.activeBottomPanelTabId
return (
activeTabId === 'shortcuts-essentials' ||
activeTabId === 'shortcuts-view-controls'
)
})
const shouldCapitalizeTab = (tabId: string): boolean => {
return tabId !== 'shortcuts-essentials' && tabId !== 'shortcuts-view-controls'
}
const getTabDisplayTitle = (tab: BottomPanelExtension): string => {
const title = tab.titleKey ? t(tab.titleKey) : tab.title || ''
return shouldCapitalizeTab(tab.id) ? title.toUpperCase() : title
}
const openKeybindingSettings = async () => {
dialogService.showSettingsDialog('keybinding')
}
const closeBottomPanel = () => {
bottomPanelStore.activePanel = null
}
</script>
<template>
<div class="flex flex-col h-full">
<Tabs
@@ -95,3 +52,46 @@ const closeBottomPanel = () => {
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Tab from 'primevue/tab'
import TabList from 'primevue/tablist'
import Tabs from 'primevue/tabs'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import { useDialogService } from '@/services/dialogService'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import type { BottomPanelExtension } from '@/types/extensionTypes'
const bottomPanelStore = useBottomPanelStore()
const dialogService = useDialogService()
const { t } = useI18n()
const isShortcutsTabActive = computed(() => {
const activeTabId = bottomPanelStore.activeBottomPanelTabId
return (
activeTabId === 'shortcuts-essentials' ||
activeTabId === 'shortcuts-view-controls'
)
})
const shouldCapitalizeTab = (tabId: string): boolean => {
return tabId !== 'shortcuts-essentials' && tabId !== 'shortcuts-view-controls'
}
const getTabDisplayTitle = (tab: BottomPanelExtension): string => {
const title = tab.titleKey ? t(tab.titleKey) : tab.title || ''
return shouldCapitalizeTab(tab.id) ? title.toUpperCase() : title
}
const openKeybindingSettings = async () => {
dialogService.showSettingsDialog('keybinding')
}
const closeBottomPanel = () => {
bottomPanelStore.activePanel = null
}
</script>

View File

@@ -1,3 +1,14 @@
<template>
<div class="h-full flex flex-col p-4">
<div class="flex-1 min-h-0 overflow-auto">
<ShortcutsList
:commands="essentialsCommands"
:subcategories="essentialsSubcategories"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
@@ -20,14 +31,3 @@ const { subcategories: essentialsSubcategories } = useCommandSubcategories(
ESSENTIALS_CONFIG
)
</script>
<template>
<div class="h-full flex flex-col p-4">
<div class="flex-1 min-h-0 overflow-auto">
<ShortcutsList
:commands="essentialsCommands"
:subcategories="essentialsSubcategories"
/>
</div>
</div>
</template>

View File

@@ -1,3 +1,50 @@
<template>
<div class="shortcuts-list flex justify-center">
<div class="grid gap-4 md:gap-24 h-full grid-cols-1 md:grid-cols-3 w-[90%]">
<div
v-for="(subcategoryCommands, subcategory) in filteredSubcategories"
:key="subcategory"
class="flex flex-col"
>
<h3
class="subcategory-title text-xs font-bold uppercase tracking-wide text-surface-600 dark-theme:text-surface-400 mb-4"
>
{{ getSubcategoryTitle(subcategory) }}
</h3>
<div class="flex flex-col gap-1">
<div
v-for="command in subcategoryCommands"
:key="command.id"
class="shortcut-item flex justify-between items-center py-2 rounded hover:bg-surface-100 dark-theme:hover:bg-surface-700 transition-colors duration-200"
>
<div class="shortcut-info grow pr-4">
<div class="shortcut-name text-sm font-medium">
{{ t(`commands.${normalizeI18nKey(command.id)}.label`) }}
</div>
</div>
<div class="keybinding-display shrink-0">
<div
class="keybinding-combo flex gap-1"
:aria-label="`Keyboard shortcut: ${command.keybinding!.combo.getKeySequences().join(' + ')}`"
>
<span
v-for="key in command.keybinding!.combo.getKeySequences()"
:key="key"
class="key-badge px-2 py-1 text-xs font-mono bg-surface-200 dark-theme:bg-surface-600 rounded border min-w-6 text-center"
>
{{ formatKey(key) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -54,53 +101,6 @@ const formatKey = (key: string): string => {
}
</script>
<template>
<div class="shortcuts-list flex justify-center">
<div class="grid gap-4 md:gap-24 h-full grid-cols-1 md:grid-cols-3 w-[90%]">
<div
v-for="(subcategoryCommands, subcategory) in filteredSubcategories"
:key="subcategory"
class="flex flex-col"
>
<h3
class="subcategory-title text-xs font-bold uppercase tracking-wide text-surface-600 dark-theme:text-surface-400 mb-4"
>
{{ getSubcategoryTitle(subcategory) }}
</h3>
<div class="flex flex-col gap-1">
<div
v-for="command in subcategoryCommands"
:key="command.id"
class="shortcut-item flex justify-between items-center py-2 rounded hover:bg-surface-100 dark-theme:hover:bg-surface-700 transition-colors duration-200"
>
<div class="shortcut-info grow pr-4">
<div class="shortcut-name text-sm font-medium">
{{ t(`commands.${normalizeI18nKey(command.id)}.label`) }}
</div>
</div>
<div class="keybinding-display shrink-0">
<div
class="keybinding-combo flex gap-1"
:aria-label="`Keyboard shortcut: ${command.keybinding!.combo.getKeySequences().join(' + ')}`"
>
<span
v-for="key in command.keybinding!.combo.getKeySequences()"
:key="key"
class="key-badge px-2 py-1 text-xs font-mono bg-surface-200 dark-theme:bg-surface-600 rounded border min-w-6 text-center"
>
{{ formatKey(key) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.subcategory-title {
color: var(--p-text-muted-color);

View File

@@ -1,3 +1,14 @@
<template>
<div class="h-full flex flex-col p-4">
<div class="flex-1 min-h-0 overflow-auto">
<ShortcutsList
:commands="viewControlsCommands"
:subcategories="viewControlsSubcategories"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
@@ -20,14 +31,3 @@ const { subcategories: viewControlsSubcategories } = useCommandSubcategories(
VIEW_CONTROLS_CONFIG
)
</script>
<template>
<div class="h-full flex flex-col p-4">
<div class="flex-1 min-h-0 overflow-auto">
<ShortcutsList
:commands="viewControlsCommands"
:subcategories="viewControlsSubcategories"
/>
</div>
</div>
</template>

View File

@@ -1,3 +1,27 @@
<template>
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
<div class="p-terminal rounded-none h-full w-full p-2">
<div ref="terminalEl" class="h-full terminal-host" />
</div>
<Button
v-tooltip.left="{
value: tooltipText,
showDelay: 300
}"
icon="pi pi-copy"
severity="secondary"
size="small"
:class="
cn('absolute top-2 right-8 transition-opacity', {
'opacity-0 pointer-events-none select-none': !isHovered
})
"
:aria-label="tooltipText"
@click="handleCopy"
/>
</div>
</template>
<script setup lang="ts">
import { useElementHover, useEventListener } from '@vueuse/core'
import type { IDisposable } from '@xterm/xterm'
@@ -73,30 +97,6 @@ onUnmounted(() => {
})
</script>
<template>
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
<div class="p-terminal rounded-none h-full w-full p-2">
<div ref="terminalEl" class="h-full terminal-host" />
</div>
<Button
v-tooltip.left="{
value: tooltipText,
showDelay: 300
}"
icon="pi pi-copy"
severity="secondary"
size="small"
:class="
cn('absolute top-2 right-8 transition-opacity', {
'opacity-0 pointer-events-none select-none': !isHovered
})
"
:aria-label="tooltipText"
@click="handleCopy"
/>
</div>
</template>
<style scoped>
:deep(.p-terminal) .xterm {
overflow-x: auto;

View File

@@ -1,3 +1,7 @@
<template>
<BaseTerminal @created="terminalCreated" />
</template>
<script setup lang="ts">
import type { IDisposable } from '@xterm/xterm'
import type { Ref } from 'vue'
@@ -54,10 +58,6 @@ const terminalCreated = (
}
</script>
<template>
<BaseTerminal @created="terminalCreated" />
</template>
<style scoped>
:deep(.p-terminal) .xterm {
overflow-x: auto;

View File

@@ -1,3 +1,16 @@
<template>
<div class="bg-black h-full w-full">
<p v-if="errorMessage" class="p-4 text-center">
{{ errorMessage }}
</p>
<ProgressSpinner
v-else-if="loading"
class="relative inset-0 flex justify-center items-center h-full z-10"
/>
<BaseTerminal v-show="!loading" @created="terminalCreated" />
</div>
</template>
<script setup lang="ts">
import { until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
@@ -75,19 +88,6 @@ const terminalCreated = (
}
</script>
<template>
<div class="bg-black h-full w-full">
<p v-if="errorMessage" class="p-4 text-center">
{{ errorMessage }}
</p>
<ProgressSpinner
v-else-if="loading"
class="relative inset-0 flex justify-center items-center h-full z-10"
/>
<BaseTerminal v-show="!loading" @created="terminalCreated" />
</div>
</template>
<style scoped>
:deep(.p-terminal) .xterm {
overflow-x: auto;

View File

@@ -1,3 +1,36 @@
<template>
<div
class="subgraph-breadcrumb w-auto"
:class="{
'subgraph-breadcrumb-collapse': collapseTabs,
'subgraph-breadcrumb-overflow': overflowingTabs
}"
:style="{
'--p-breadcrumb-gap': `${ITEM_GAP}px`,
'--p-breadcrumb-item-min-width': `${MIN_WIDTH}px`,
'--p-breadcrumb-item-padding': `${ITEM_PADDING}px`,
'--p-breadcrumb-icon-width': `${ICON_WIDTH}px`
}"
>
<Breadcrumb
ref="breadcrumbRef"
class="bg-transparent p-0"
:model="items"
aria-label="Graph navigation"
>
<template #item="{ item }">
<SubgraphBreadcrumbItem
:item="item"
:is-active="item === items.at(-1)"
/>
</template>
<template #separator
><span style="transform: scale(1.5)"> / </span></template
>
</Breadcrumb>
</div>
</template>
<script setup lang="ts">
import Breadcrumb from 'primevue/breadcrumb'
import type { MenuItem } from 'primevue/menuitem'
@@ -127,39 +160,6 @@ onUpdated(() => {
})
</script>
<template>
<div
class="subgraph-breadcrumb w-auto"
:class="{
'subgraph-breadcrumb-collapse': collapseTabs,
'subgraph-breadcrumb-overflow': overflowingTabs
}"
:style="{
'--p-breadcrumb-gap': `${ITEM_GAP}px`,
'--p-breadcrumb-item-min-width': `${MIN_WIDTH}px`,
'--p-breadcrumb-item-padding': `${ITEM_PADDING}px`,
'--p-breadcrumb-icon-width': `${ICON_WIDTH}px`
}"
>
<Breadcrumb
ref="breadcrumbRef"
class="bg-transparent p-0"
:model="items"
aria-label="Graph navigation"
>
<template #item="{ item }">
<SubgraphBreadcrumbItem
:item="item"
:is-active="item === items.at(-1)"
/>
</template>
<template #separator
><span style="transform: scale(1.5)"> / </span></template
>
</Breadcrumb>
</div>
</template>
<style scoped>
@reference '../../assets/css/style.css';

View File

@@ -1,3 +1,50 @@
<template>
<a
ref="wrapperRef"
v-tooltip.bottom="{
value: item.label,
showDelay: 512
}"
href="#"
class="cursor-pointer p-breadcrumb-item-link"
:class="{
'flex items-center gap-1': isActive,
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
'p-breadcrumb-item-link-icon-visible': isActive,
'active-breadcrumb-item': isActive
}"
@click="handleClick"
>
<span class="p-breadcrumb-item-label">{{ item.label }}</span>
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
</a>
<Menu
v-if="isActive"
ref="menu"
:model="menuItems"
:popup="true"
:pt="{
root: {
style: 'background-color: var(--comfy-menu-secondary-bg)'
},
itemLink: {
class: 'py-2'
}
}"
/>
<InputText
v-if="isEditing"
ref="itemInputRef"
v-model="itemLabel"
class="fixed z-10000 text-[.8rem] px-2 py-2"
@blur="inputBlur(true)"
@click.stop
@keydown.enter="inputBlur(true)"
@keydown.esc="inputBlur(false)"
/>
</template>
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import type { MenuState } from 'primevue/menu'
@@ -183,53 +230,6 @@ const inputBlur = async (doRename: boolean) => {
}
</script>
<template>
<a
ref="wrapperRef"
v-tooltip.bottom="{
value: item.label,
showDelay: 512
}"
href="#"
class="cursor-pointer p-breadcrumb-item-link"
:class="{
'flex items-center gap-1': isActive,
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
'p-breadcrumb-item-link-icon-visible': isActive,
'active-breadcrumb-item': isActive
}"
@click="handleClick"
>
<span class="p-breadcrumb-item-label">{{ item.label }}</span>
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
</a>
<Menu
v-if="isActive"
ref="menu"
:model="menuItems"
:popup="true"
:pt="{
root: {
style: 'background-color: var(--comfy-menu-secondary-bg)'
},
itemLink: {
class: 'py-2'
}
}"
/>
<InputText
v-if="isEditing"
ref="itemInputRef"
v-model="itemLabel"
class="fixed z-10000 text-[.8rem] px-2 py-2"
@blur="inputBlur(true)"
@click.stop
@keydown.enter="inputBlur(true)"
@keydown.esc="inputBlur(false)"
/>
</template>
<style scoped>
@reference '../../assets/css/style.css';

View File

@@ -1,3 +1,15 @@
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<slot></slot>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
@@ -38,15 +50,3 @@ const buttonStyle = computed(() => {
return cn(baseClasses, sizeClasses, typeClasses, className)
})
</script>
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<slot></slot>
</Button>
</template>

View File

@@ -1,3 +1,9 @@
<template>
<div :class="iconGroupClasses">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { cn } from '@/utils/tailwindUtil'
@@ -9,9 +15,3 @@ const iconGroupClasses = cn(
'cursor-pointer'
)
</script>
<template>
<div :class="iconGroupClasses">
<slot></slot>
</div>
</template>

View File

@@ -1,3 +1,17 @@
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
<span>{{ label }}</span>
<slot v-if="iconPosition === 'right'" name="icon"></slot>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
@@ -42,17 +56,3 @@ const buttonStyle = computed(() => {
return cn(baseClasses, sizeClasses, typeClasses, className)
})
</script>
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
<span>{{ label }}</span>
<slot v-if="iconPosition === 'right'" name="icon"></slot>
</Button>
</template>

View File

@@ -1,3 +1,26 @@
<template>
<div class="relative inline-flex items-center">
<IconButton @click="toggle">
<i-lucide:more-vertical class="text-sm" />
</IconButton>
<Popover
ref="popover"
:append-to="'body'"
:auto-z-index="true"
:base-z-index="1000"
:dismissable="true"
:close-on-escape="true"
unstyled
:pt="pt"
>
<div class="flex flex-col gap-2 p-2 min-w-40">
<slot :close="hide" />
</div>
</Popover>
</div>
</template>
<script setup lang="ts">
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
@@ -31,26 +54,3 @@ const pt = computed(() => ({
}
}))
</script>
<template>
<div class="relative inline-flex items-center">
<IconButton @click="toggle">
<i-lucide:more-vertical class="text-sm" />
</IconButton>
<Popover
ref="popover"
:append-to="'body'"
:auto-z-index="true"
:base-z-index="1000"
:dismissable="true"
:close-on-escape="true"
unstyled
:pt="pt"
>
<div class="flex flex-col gap-2 p-2 min-w-40">
<slot :close="hide" />
</div>
</Popover>
</div>
</template>

View File

@@ -1,3 +1,15 @@
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<span>{{ label }}</span>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
@@ -40,15 +52,3 @@ const buttonStyle = computed(() => {
return cn(baseClasses, sizeClasses, typeClasses, className)
})
</script>
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<span>{{ label }}</span>
</Button>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"></script>
<template>
<div class="flex-1 w-full h-full">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -1,3 +1,10 @@
<template>
<div :class="containerClasses">
<slot name="top"></slot>
<slot name="bottom"></slot>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
@@ -18,10 +25,3 @@ const containerClasses = computed(() => {
return `${baseClasses} ${ratioClasses[ratio]}`
})
</script>
<template>
<div :class="containerClasses">
<slot name="top"></slot>
<slot name="bottom"></slot>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"></script>
<template>
<div class="text-zinc-500 dark-theme:text-zinc-400 text-xs line-clamp-2 h-7">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"></script>
<template>
<div class="text-neutral text-sm">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -1,22 +1,3 @@
<script setup lang="ts">
import { computed } from 'vue'
const { ratio = 'square' } = defineProps<{
ratio?: 'square' | 'landscape'
}>()
const topStyle = computed(() => {
const baseClasses = 'relative p-0'
const ratioClasses = {
square: 'aspect-square',
landscape: 'aspect-48/27'
}
return `${baseClasses} ${ratioClasses[ratio]}`
})
</script>
<template>
<div :class="topStyle">
<slot class="absolute top-0 left-0 w-full h-full"></slot>
@@ -38,3 +19,22 @@ const topStyle = computed(() => {
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const { ratio = 'square' } = defineProps<{
ratio?: 'square' | 'landscape'
}>()
const topStyle = computed(() => {
const baseClasses = 'relative p-0'
const ratioClasses = {
square: 'aspect-square',
landscape: 'aspect-48/27'
}
return `${baseClasses} ${ratioClasses[ratio]}`
})
</script>

View File

@@ -1,8 +1,3 @@
<script setup lang="ts">
const { label } = defineProps<{
label: string
}>()
</script>
<template>
<div
class="inline-flex justify-center items-center gap-1 shrink-0 py-1 px-2 text-xs bg-[#D9D9D966]/40 rounded font-bold text-white/90"
@@ -11,3 +6,8 @@ const { label } = defineProps<{
<span>{{ label }}</span>
</div>
</template>
<script setup lang="ts">
const { label } = defineProps<{
label: string
}>()
</script>

View File

@@ -1,3 +1,36 @@
<template>
<div class="flex gap-2">
<InputText
v-model="modelValue"
class="flex-1"
:placeholder="$t('g.imageUrl')"
/>
<Button
v-tooltip="$t('g.upload')"
:icon="isUploading ? 'pi pi-spin pi-spinner' : 'pi pi-upload'"
size="small"
:disabled="isUploading"
@click="triggerFileInput"
/>
<Button
v-tooltip="$t('g.clear')"
outlined
icon="pi pi-trash"
severity="danger"
size="small"
:disabled="!modelValue"
@click="clearImage"
/>
<input
ref="fileInput"
type="file"
class="hidden"
accept="image/*"
@change="handleFileUpload"
/>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
@@ -68,36 +101,3 @@ const clearImage = () => {
}
}
</script>
<template>
<div class="flex gap-2">
<InputText
v-model="modelValue"
class="flex-1"
:placeholder="$t('g.imageUrl')"
/>
<Button
v-tooltip="$t('g.upload')"
:icon="isUploading ? 'pi pi-spin pi-spinner' : 'pi pi-upload'"
size="small"
:disabled="isUploading"
@click="triggerFileInput"
/>
<Button
v-tooltip="$t('g.clear')"
outlined
icon="pi pi-trash"
severity="danger"
size="small"
:disabled="!modelValue"
@click="clearImage"
/>
<input
ref="fileInput"
type="file"
class="hidden"
accept="image/*"
@change="handleFileUpload"
/>
</div>
</template>

View File

@@ -1,3 +1,34 @@
<template>
<div
class="color-customization-selector-container flex flex-row items-center gap-2"
>
<SelectButton
v-model="selectedColorOption"
:options="colorOptionsWithCustom"
option-label="name"
data-key="value"
:allow-empty="false"
>
<template #option="slotProps">
<div
v-if="slotProps.option.name !== '_custom'"
:style="{
width: '20px',
height: '20px',
backgroundColor: slotProps.option.value,
borderRadius: '50%'
}"
/>
<i v-else class="pi pi-palette text-lg" />
</template>
</SelectButton>
<ColorPicker
v-if="selectedColorOption.name === '_custom'"
v-model="customColorValue"
/>
</div>
</template>
<script setup lang="ts">
import ColorPicker from 'primevue/colorpicker'
import SelectButton from 'primevue/selectbutton'
@@ -55,34 +86,3 @@ watch(customColorValue, (newValue) => {
}
})
</script>
<template>
<div
class="color-customization-selector-container flex flex-row items-center gap-2"
>
<SelectButton
v-model="selectedColorOption"
:options="colorOptionsWithCustom"
option-label="name"
data-key="value"
:allow-empty="false"
>
<template #option="slotProps">
<div
v-if="slotProps.option.name !== '_custom'"
:style="{
width: '20px',
height: '20px',
backgroundColor: slotProps.option.value,
borderRadius: '50%'
}"
/>
<i v-else class="pi pi-palette text-lg" />
</template>
</SelectButton>
<ColorPicker
v-if="selectedColorOption.name === '_custom'"
v-model="customColorValue"
/>
</div>
</template>

View File

@@ -1,25 +1,4 @@
<!-- A image with placeholder fallback on error -->
<script setup lang="ts">
import { ref } from 'vue'
const {
src,
class: classProp,
contain = false,
alt = 'Image content'
} = defineProps<{
src: string
class?: any
contain?: boolean
alt?: string
}>()
const imageBroken = ref(false)
const handleImageError = () => {
imageBroken.value = true
}
</script>
<template>
<span
v-if="!imageBroken"
@@ -49,6 +28,27 @@ const handleImageError = () => {
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const {
src,
class: classProp,
contain = false,
alt = 'Image content'
} = defineProps<{
src: string
class?: any
contain?: boolean
alt?: string
}>()
const imageBroken = ref(false)
const handleImageError = () => {
imageBroken.value = true
}
</script>
<style scoped>
.comfy-image-wrap {
display: contents;

View File

@@ -1,3 +1,16 @@
<template>
<div
:class="{
'content-divider': true,
'content-divider--horizontal': orientation === 'horizontal',
'content-divider--vertical': orientation === 'vertical'
}"
:style="{
backgroundColor: isLightTheme ? '#DCDAE1' : '#2C2C2C'
}"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
@@ -14,19 +27,6 @@ const isLightTheme = computed(
)
</script>
<template>
<div
:class="{
'content-divider': true,
'content-divider--horizontal': orientation === 'horizontal',
'content-divider--vertical': orientation === 'vertical'
}"
:style="{
backgroundColor: isLightTheme ? '#DCDAE1' : '#2C2C2C'
}"
/>
</template>
<style scoped>
.content-divider {
display: inline-block;

View File

@@ -1,3 +1,7 @@
<template>
<div ref="container" />
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
@@ -19,7 +23,3 @@ onMounted(renderContent)
watch(() => props.renderFunction, renderContent)
</script>
<template>
<div ref="container" />
</template>

View File

@@ -1,3 +1,48 @@
<template>
<Dialog v-model:visible="visible" :header="$t('g.customizeFolder')">
<div class="p-fluid">
<div class="field icon-field">
<label for="icon">{{ $t('g.icon') }}</label>
<SelectButton
v-model="selectedIcon"
:options="iconOptions"
option-label="name"
data-key="value"
>
<template #option="slotProps">
<i
:class="['pi', slotProps.option.value, 'mr-2']"
:style="{ color: finalColor }"
/>
</template>
</SelectButton>
</div>
<Divider />
<div class="field color-field">
<label for="color">{{ $t('g.color') }}</label>
<ColorCustomizationSelector
v-model="finalColor"
:color-options="colorOptions"
/>
</div>
</div>
<template #footer>
<Button
:label="$t('g.reset')"
icon="pi pi-refresh"
class="p-button-text"
@click="resetCustomization"
/>
<Button
:label="$t('g.confirm')"
icon="pi pi-check"
autofocus
@click="confirmCustomization"
/>
</template>
</Dialog>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
@@ -88,51 +133,6 @@ watch(
)
</script>
<template>
<Dialog v-model:visible="visible" :header="$t('g.customizeFolder')">
<div class="p-fluid">
<div class="field icon-field">
<label for="icon">{{ $t('g.icon') }}</label>
<SelectButton
v-model="selectedIcon"
:options="iconOptions"
option-label="name"
data-key="value"
>
<template #option="slotProps">
<i
:class="['pi', slotProps.option.value, 'mr-2']"
:style="{ color: finalColor }"
/>
</template>
</SelectButton>
</div>
<Divider />
<div class="field color-field">
<label for="color">{{ $t('g.color') }}</label>
<ColorCustomizationSelector
v-model="finalColor"
:color-options="colorOptions"
/>
</div>
</div>
<template #footer>
<Button
:label="$t('g.reset')"
icon="pi pi-refresh"
class="p-button-text"
@click="resetCustomization"
/>
<Button
:label="$t('g.confirm')"
icon="pi pi-check"
autofocus
@click="confirmCustomization"
/>
</template>
</Dialog>
</template>
<style scoped>
.p-selectbutton .p-button {
padding: 0.5rem;

View File

@@ -1,3 +1,16 @@
<template>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in deviceColumns" :key="col.field">
<div class="font-medium">
{{ col.header }}
</div>
<div>
{{ formatValue(props.device[col.field], col.field) }}
</div>
</template>
</div>
</template>
<script setup lang="ts">
import type { DeviceStats } from '@/schemas/apiSchema'
import { formatSize } from '@/utils/formatUtil'
@@ -26,16 +39,3 @@ const formatValue = (value: any, field: string) => {
return value
}
</script>
<template>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in deviceColumns" :key="col.field">
<div class="font-medium">
{{ col.header }}
</div>
<div>
{{ formatValue(props.device[col.field], col.field) }}
</div>
</template>
</div>
</template>

View File

@@ -1,20 +1,3 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
const { size = 24, duration = '2s' } = defineProps<{
size?: number
duration?: string
}>()
const colorPaletteStore = useColorPaletteStore()
const color = computed(() =>
colorPaletteStore.completedActivePalette.light_theme ? '#2C2B30' : '#D4D4D4'
)
</script>
<template>
<div
class="inline-flex items-center justify-center"
@@ -112,6 +95,23 @@ const color = computed(() =>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
const { size = 24, duration = '2s' } = defineProps<{
size?: number
duration?: string
}>()
const colorPaletteStore = useColorPaletteStore()
const color = computed(() =>
colorPaletteStore.completedActivePalette.light_theme ? '#2C2B30' : '#D4D4D4'
)
</script>
<style scoped>
.dot-animation {
animation: dot-pulse 1s ease-in-out infinite;

View File

@@ -1,3 +1,30 @@
<template>
<div class="editable-text">
<span v-if="!isEditing">
{{ modelValue }}
</span>
<!-- Avoid double triggering finishEditing event when keyup.enter is triggered -->
<InputText
v-else
ref="inputRef"
v-model:model-value="inputValue"
v-focus
type="text"
size="small"
fluid
:pt="{
root: {
onBlur: finishEditing,
...inputAttrs
}
}"
@keyup.enter="blurInputElement"
@keyup.escape="cancelEditing"
@click.stop
/>
</div>
</template>
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import { nextTick, ref, watch } from 'vue'
@@ -63,33 +90,6 @@ const vFocus = {
}
</script>
<template>
<div class="editable-text">
<span v-if="!isEditing">
{{ modelValue }}
</span>
<!-- Avoid double triggering finishEditing event when keyup.enter is triggered -->
<InputText
v-else
ref="inputRef"
v-model:model-value="inputValue"
v-focus
type="text"
size="small"
fluid
:pt="{
root: {
onBlur: finishEditing,
...inputAttrs
}
}"
@keyup.enter="blurInputElement"
@keyup.escape="cancelEditing"
@click.stop
/>
</div>
</template>
<style scoped>
.editable-text {
display: inline;

View File

@@ -1,58 +1,4 @@
<!-- A Electron-backed download button with a label, size hint and progress bar -->
<script setup lang="ts">
import Button from 'primevue/button'
import ProgressBar from 'primevue/progressbar'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDownload } from '@/composables/useDownload'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
url: string
hint?: string
label?: string
error?: string
}>()
const { t } = useI18n()
const label = computed(() => props.label || props.url.split('/').pop())
const hint = computed(() => props.hint || props.url)
const download = useDownload(props.url)
const downloadProgress = ref<number>(0)
const status = ref<string | null>(null)
const fileSize = computed(() =>
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
)
const electronDownloadStore = useElectronDownloadStore()
// @ts-expect-error fixme ts strict error
const [savePath, filename] = props.label.split('/')
electronDownloadStore.$subscribe((_, { downloads }) => {
const download = downloads.find((download) => props.url === download.url)
if (download) {
// @ts-expect-error fixme ts strict error
downloadProgress.value = Number((download.progress * 100).toFixed(1))
// @ts-expect-error fixme ts strict error
status.value = download.status
}
})
const triggerDownload = async () => {
await electronDownloadStore.start({
url: props.url,
savePath: savePath.trim(),
filename: filename.trim()
})
}
const triggerCancelDownload = () => electronDownloadStore.cancel(props.url)
const triggerPauseDownload = () => electronDownloadStore.pause(props.url)
const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
</script>
<template>
<div class="flex flex-col">
<div class="flex flex-row items-center gap-2">
@@ -127,3 +73,57 @@ const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ProgressBar from 'primevue/progressbar'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDownload } from '@/composables/useDownload'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
url: string
hint?: string
label?: string
error?: string
}>()
const { t } = useI18n()
const label = computed(() => props.label || props.url.split('/').pop())
const hint = computed(() => props.hint || props.url)
const download = useDownload(props.url)
const downloadProgress = ref<number>(0)
const status = ref<string | null>(null)
const fileSize = computed(() =>
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
)
const electronDownloadStore = useElectronDownloadStore()
// @ts-expect-error fixme ts strict error
const [savePath, filename] = props.label.split('/')
electronDownloadStore.$subscribe((_, { downloads }) => {
const download = downloads.find((download) => props.url === download.url)
if (download) {
// @ts-expect-error fixme ts strict error
downloadProgress.value = Number((download.progress * 100).toFixed(1))
// @ts-expect-error fixme ts strict error
status.value = download.status
}
})
const triggerDownload = async () => {
await electronDownloadStore.start({
url: props.url,
savePath: savePath.trim(),
filename: filename.trim()
})
}
const triggerCancelDownload = () => electronDownloadStore.cancel(props.url)
const triggerPauseDownload = () => electronDownloadStore.pause(props.url)
const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
</script>

View File

@@ -1,3 +1,19 @@
<template>
<component :is="extension.component" v-if="extension.type === 'vue'" />
<div
v-else
:ref="
(el) => {
if (el)
mountCustomExtension(
props.extension as CustomExtension,
el as HTMLElement
)
}
"
/>
</template>
<script setup lang="ts">
import { onBeforeUnmount } from 'vue'
@@ -17,19 +33,3 @@ onBeforeUnmount(() => {
}
})
</script>
<template>
<component :is="extension.component" v-if="extension.type === 'vue'" />
<div
v-else
:ref="
(el) => {
if (el)
mountCustomExtension(
props.extension as CustomExtension,
el as HTMLElement
)
}
"
/>
</template>

View File

@@ -1,34 +1,4 @@
<!-- A file download button with a label and a size hint -->
<script setup lang="ts">
import Button from 'primevue/button'
import Message from 'primevue/message'
import { computed } from 'vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
url: string
hint?: string
label?: string
error?: string
}>()
const label = computed(() => props.label || props.url.split('/').pop())
const hint = computed(() => props.hint || props.url)
const download = useDownload(props.url)
const fileSize = computed(() =>
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
)
const copyURL = async () => {
await copyToClipboard(props.url)
}
const { copyToClipboard } = useCopyToClipboard()
</script>
<template>
<div class="flex flex-row items-center gap-2">
<div>
@@ -71,3 +41,33 @@ const { copyToClipboard } = useCopyToClipboard()
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Message from 'primevue/message'
import { computed } from 'vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
url: string
hint?: string
label?: string
error?: string
}>()
const label = computed(() => props.label || props.url.split('/').pop())
const hint = computed(() => props.hint || props.url)
const download = useDownload(props.url)
const fileSize = computed(() =>
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
)
const copyURL = async () => {
await copyToClipboard(props.url)
}
const { copyToClipboard } = useCopyToClipboard()
</script>

View File

@@ -1,3 +1,10 @@
<template>
<div class="color-picker-wrapper flex items-center gap-2">
<ColorPicker v-model="modelValue" v-bind="$attrs" />
<InputText v-model="modelValue" class="w-28" :placeholder="label" />
</div>
</template>
<script setup lang="ts">
import ColorPicker from 'primevue/colorpicker'
import InputText from 'primevue/inputtext'
@@ -12,10 +19,3 @@ defineOptions({
inheritAttrs: false
})
</script>
<template>
<div class="color-picker-wrapper flex items-center gap-2">
<ColorPicker v-model="modelValue" v-bind="$attrs" />
<InputText v-model="modelValue" class="w-28" :placeholder="label" />
</div>
</template>

View File

@@ -1,41 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { ref } from 'vue'
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const fileInput = ref<HTMLInputElement | null>(null)
const triggerFileInput = () => {
fileInput.value?.click()
}
const handleFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files && target.files[0]) {
const file = target.files[0]
const reader = new FileReader()
reader.onload = (e) => {
emit('update:modelValue', e.target?.result as string)
}
reader.readAsDataURL(file)
}
}
const clearImage = () => {
emit('update:modelValue', '')
if (fileInput.value) {
fileInput.value.value = ''
}
}
</script>
<template>
<div class="image-upload-wrapper">
<div class="flex gap-2 items-center">
@@ -78,3 +40,41 @@ const clearImage = () => {
/>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { ref } from 'vue'
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const fileInput = ref<HTMLInputElement | null>(null)
const triggerFileInput = () => {
fileInput.value?.click()
}
const handleFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files && target.files[0]) {
const file = target.files[0]
const reader = new FileReader()
reader.onload = (e) => {
emit('update:modelValue', e.target?.result as string)
}
reader.readAsDataURL(file)
}
}
const clearImage = () => {
emit('update:modelValue', '')
if (fileInput.value) {
fileInput.value.value = ''
}
}
</script>

View File

@@ -1,4 +1,34 @@
<!-- A generalized form item for rendering in a form. -->
<template>
<div class="flex flex-row items-center gap-2">
<div class="form-label flex grow items-center">
<span
:id="`${props.id}-label`"
class="text-muted"
:class="props.labelClass"
>
<slot name="name-prefix" />
{{ props.item.name }}
<i
v-if="props.item.tooltip"
v-tooltip="props.item.tooltip"
class="pi pi-info-circle bg-transparent"
/>
<slot name="name-suffix" />
</span>
</div>
<div class="form-input flex justify-end">
<component
:is="markRaw(getFormComponent(props.item))"
:id="props.id"
v-model:model-value="formValue"
:aria-labelledby="`${props.id}-label`"
v-bind="getFormAttrs(props.item)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import InputNumber from 'primevue/inputnumber'
import InputText from 'primevue/inputtext'
@@ -85,36 +115,6 @@ function getFormComponent(item: FormItem): Component {
}
</script>
<template>
<div class="flex flex-row items-center gap-2">
<div class="form-label flex grow items-center">
<span
:id="`${props.id}-label`"
class="text-muted"
:class="props.labelClass"
>
<slot name="name-prefix" />
{{ props.item.name }}
<i
v-if="props.item.tooltip"
v-tooltip="props.item.tooltip"
class="pi pi-info-circle bg-transparent"
/>
<slot name="name-suffix" />
</span>
</div>
<div class="form-input flex justify-end">
<component
:is="markRaw(getFormComponent(props.item))"
:id="props.id"
v-model:model-value="formValue"
:aria-labelledby="`${props.id}-label`"
v-bind="getFormAttrs(props.item)"
/>
</div>
</div>
</template>
<style scoped>
@reference '../../assets/css/style.css';

View File

@@ -1,3 +1,25 @@
<template>
<div class="flex flex-row gap-4">
<div
v-for="option in normalizedOptions"
:key="option.value"
class="flex items-center"
>
<RadioButton
:input-id="`${id}-${option.value}`"
:name="id"
:value="option.value"
:model-value="modelValue"
:aria-describedby="`${option.text}-label`"
@update:model-value="$emit('update:modelValue', $event)"
/>
<label :for="`${id}-${option.value}`" class="ml-2 cursor-pointer">
{{ option.text }}
</label>
</div>
</div>
</template>
<script setup lang="ts">
import RadioButton from 'primevue/radiobutton'
import { computed } from 'vue'
@@ -38,25 +60,3 @@ const normalizedOptions = computed<SettingOption[]>(() => {
})
})
</script>
<template>
<div class="flex flex-row gap-4">
<div
v-for="option in normalizedOptions"
:key="option.value"
class="flex items-center"
>
<RadioButton
:input-id="`${id}-${option.value}`"
:name="id"
:value="option.value"
:model-value="modelValue"
:aria-describedby="`${option.text}-label`"
@update:model-value="$emit('update:modelValue', $event)"
/>
<label :for="`${id}-${option.value}`" class="ml-2 cursor-pointer">
{{ option.text }}
</label>
</div>
</div>
</template>

View File

@@ -1,3 +1,30 @@
<template>
<div class="input-knob flex flex-row items-center gap-2">
<Knob
:model-value="modelValue"
:value-template="displayValue"
class="knob-part"
:class="knobClass"
:min="min"
:max="max"
:step="step"
v-bind="$attrs"
@update:model-value="updateValue"
/>
<InputNumber
:model-value="modelValue"
class="input-part"
:max-fraction-digits="3"
:class="inputClass"
:min="min"
:max="max"
:step="step"
:allow-empty="false"
@update:model-value="updateValue"
/>
</div>
</template>
<script setup lang="ts">
import InputNumber from 'primevue/inputnumber'
import Knob from 'primevue/knob'
@@ -60,30 +87,3 @@ defineOptions({
inheritAttrs: false
})
</script>
<template>
<div class="input-knob flex flex-row items-center gap-2">
<Knob
:model-value="modelValue"
:value-template="displayValue"
class="knob-part"
:class="knobClass"
:min="min"
:max="max"
:step="step"
v-bind="$attrs"
@update:model-value="updateValue"
/>
<InputNumber
:model-value="modelValue"
class="input-part"
:max-fraction-digits="3"
:class="inputClass"
:min="min"
:max="max"
:step="step"
:allow-empty="false"
@update:model-value="updateValue"
/>
</div>
</template>

View File

@@ -1,3 +1,29 @@
<template>
<div class="input-slider flex flex-row items-center gap-2">
<Slider
:model-value="modelValue"
class="slider-part"
:class="sliderClass"
:min="min"
:max="max"
:step="step"
v-bind="$attrs"
@update:model-value="(value) => updateValue(value as number)"
/>
<InputNumber
:model-value="modelValue"
class="input-part"
:max-fraction-digits="3"
:class="inputClass"
:min="min"
:max="max"
:step="step"
:allow-empty="false"
@update:model-value="updateValue"
/>
</div>
</template>
<script setup lang="ts">
import InputNumber from 'primevue/inputnumber'
import Slider from 'primevue/slider'
@@ -50,29 +76,3 @@ defineOptions({
inheritAttrs: false
})
</script>
<template>
<div class="input-slider flex flex-row items-center gap-2">
<Slider
:model-value="modelValue"
class="slider-part"
:class="sliderClass"
:min="min"
:max="max"
:step="step"
v-bind="$attrs"
@update:model-value="(value) => updateValue(value as number)"
/>
<InputNumber
:model-value="modelValue"
class="input-part"
:max-fraction-digits="3"
:class="inputClass"
:min="min"
:max="max"
:step="step"
:allow-empty="false"
@update:model-value="updateValue"
/>
</div>
</template>

View File

@@ -1,3 +1,34 @@
<template>
<div
ref="containerRef"
class="relative overflow-hidden w-full h-full flex items-center justify-center"
>
<Skeleton
v-if="!isImageLoaded"
width="100%"
height="100%"
class="absolute inset-0"
/>
<img
v-if="cachedSrc"
ref="imageRef"
:src="cachedSrc"
:alt="alt"
draggable="false"
:class="imageClass"
:style="imageStyle"
@load="onImageLoad"
@error="onImageError"
/>
<div
v-if="hasError"
class="absolute inset-0 flex items-center justify-center bg-surface-50 dark-theme:bg-surface-800 text-muted"
>
<i class="pi pi-image text-2xl" />
</div>
</div>
</template>
<script setup lang="ts">
import Skeleton from 'primevue/skeleton'
import { computed, onUnmounted, ref, watch } from 'vue'
@@ -91,34 +122,3 @@ onUnmounted(() => {
}
})
</script>
<template>
<div
ref="containerRef"
class="relative overflow-hidden w-full h-full flex items-center justify-center"
>
<Skeleton
v-if="!isImageLoaded"
width="100%"
height="100%"
class="absolute inset-0"
/>
<img
v-if="cachedSrc"
ref="imageRef"
:src="cachedSrc"
:alt="alt"
draggable="false"
:class="imageClass"
:style="imageStyle"
@load="onImageLoad"
@error="onImageError"
/>
<div
v-if="hasError"
class="absolute inset-0 flex items-center justify-center bg-surface-50 dark-theme:bg-surface-800 text-muted"
>
<i class="pi pi-image text-2xl" />
</div>
</div>
</template>

View File

@@ -1,19 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Card from 'primevue/card'
const props = defineProps<{
class?: string
icon?: string
title: string
message: string
textClass?: string
buttonLabel?: string
}>()
defineEmits(['action'])
</script>
<template>
<div class="no-results-placeholder p-8 h-full" :class="props.class">
<Card>
@@ -36,6 +20,22 @@ defineEmits(['action'])
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Card from 'primevue/card'
const props = defineProps<{
class?: string
icon?: string
title: string
message: string
textClass?: string
buttonLabel?: string
}>()
defineEmits(['action'])
</script>
<style scoped>
.no-results-placeholder :deep(.p-card) {
background-color: var(--surface-ground);

View File

@@ -10,6 +10,24 @@
/>
```
-->
<template>
<Button
class="relative p-button-icon-only"
:outlined="outlined"
:severity="severity"
:disabled="active || disabled"
@click="(event) => $emit('refresh', event)"
>
<span
class="p-button-icon pi pi-refresh transition-all"
:class="{ 'opacity-0': active }"
data-pc-section="icon"
/>
<span class="p-button-label" data-pc-section="label">&nbsp;</span>
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ProgressSpinner from 'primevue/progressspinner'
@@ -32,21 +50,3 @@ const active = defineModel<boolean>({ required: true })
// Emits
defineEmits(['refresh'])
</script>
<template>
<Button
class="relative p-button-icon-only"
:outlined="outlined"
:severity="severity"
:disabled="active || disabled"
@click="(event) => $emit('refresh', event)"
>
<span
class="p-button-icon pi pi-refresh transition-all"
:class="{ 'opacity-0': active }"
data-pc-section="icon"
/>
<span class="p-button-label" data-pc-section="label">&nbsp;</span>
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
</Button>
</template>

View File

@@ -1,3 +1,46 @@
<template>
<div>
<IconField>
<Button
v-if="filterIcon"
class="p-inputicon filter-button"
:icon="filterIcon"
text
severity="contrast"
@click="$emit('showFilter', $event)"
/>
<InputText
class="search-box-input w-full"
:model-value="modelValue"
:placeholder="placeholder"
@input="handleInput"
/>
<InputIcon v-if="!modelValue" :class="icon" />
<Button
v-if="modelValue"
class="p-inputicon clear-button"
icon="pi pi-times"
text
severity="contrast"
@click="clearSearch"
/>
</IconField>
<div
v-if="filters?.length"
class="search-filters pt-2 flex flex-wrap gap-2"
>
<SearchFilterChip
v-for="filter in filters"
:key="filter.id"
:text="filter.text"
:badge="filter.badge"
:badge-class="filter.badgeClass"
@remove="$emit('removeFilter', filter)"
/>
</div>
</div>
</template>
<script setup lang="ts" generic="TFilter extends SearchFilter">
import { debounce } from 'es-toolkit/compat'
import Button from 'primevue/button'
@@ -47,49 +90,6 @@ const clearSearch = () => {
}
</script>
<template>
<div>
<IconField>
<Button
v-if="filterIcon"
class="p-inputicon filter-button"
:icon="filterIcon"
text
severity="contrast"
@click="$emit('showFilter', $event)"
/>
<InputText
class="search-box-input w-full"
:model-value="modelValue"
:placeholder="placeholder"
@input="handleInput"
/>
<InputIcon v-if="!modelValue" :class="icon" />
<Button
v-if="modelValue"
class="p-inputicon clear-button"
icon="pi pi-times"
text
severity="contrast"
@click="clearSearch"
/>
</IconField>
<div
v-if="filters?.length"
class="search-filters pt-2 flex flex-wrap gap-2"
>
<SearchFilterChip
v-for="filter in filters"
:key="filter.id"
:text="filter.text"
:badge="filter.badge"
:badge-class="filter.badgeClass"
@remove="$emit('removeFilter', filter)"
/>
</div>
</div>
</template>
<style scoped>
@reference '../../assets/css/style.css';

View File

@@ -1,3 +1,12 @@
<template>
<Chip removable @remove="$emit('remove', $event)">
<Badge size="small" :class="badgeClass">
{{ badge }}
</Badge>
{{ text }}
</Chip>
</template>
<script setup lang="ts">
import Badge from 'primevue/badge'
import Chip from 'primevue/chip'
@@ -13,15 +22,6 @@ defineProps<Omit<SearchFilter, 'id'>>()
defineEmits(['remove'])
</script>
<template>
<Chip removable @remove="$emit('remove', $event)">
<Badge size="small" :class="badgeClass">
{{ badge }}
</Badge>
{{ text }}
</Chip>
</template>
<style scoped>
@reference '../../assets/css/style.css';

View File

@@ -1,3 +1,40 @@
<template>
<div class="system-stats">
<div class="mb-6">
<h2 class="text-2xl font-semibold mb-4">
{{ $t('g.systemInfo') }}
</h2>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in systemColumns" :key="col.field">
<div class="font-medium">
{{ col.header }}
</div>
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
</template>
</div>
</div>
<Divider />
<div>
<h2 class="text-2xl font-semibold mb-4">
{{ $t('g.devices') }}
</h2>
<TabView v-if="props.stats.devices.length > 1">
<TabPanel
v-for="device in props.stats.devices"
:key="device.index"
:header="device.name"
:value="device.index"
>
<DeviceInfo :device="device" />
</TabPanel>
</TabView>
<DeviceInfo v-else :device="props.stats.devices[0]" />
</div>
</div>
</template>
<script setup lang="ts">
import Divider from 'primevue/divider'
import TabPanel from 'primevue/tabpanel'
@@ -35,40 +72,3 @@ const formatValue = (value: any, field: string) => {
return value
}
</script>
<template>
<div class="system-stats">
<div class="mb-6">
<h2 class="text-2xl font-semibold mb-4">
{{ $t('g.systemInfo') }}
</h2>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in systemColumns" :key="col.field">
<div class="font-medium">
{{ col.header }}
</div>
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
</template>
</div>
</div>
<Divider />
<div>
<h2 class="text-2xl font-semibold mb-4">
{{ $t('g.devices') }}
</h2>
<TabView v-if="props.stats.devices.length > 1">
<TabPanel
v-for="device in props.stats.devices"
:key="device.index"
:header="device.name"
:value="device.index"
>
<DeviceInfo :device="device" />
</TabPanel>
</TabView>
<DeviceInfo v-else :device="props.stats.devices[0]" />
</div>
</div>
</template>

View File

@@ -1,3 +1,11 @@
<template>
<div class="flex items-center">
<span v-if="position === 'left'" class="mr-2 shrink-0">{{ text }}</span>
<Divider :align="align" :type="type" :layout="layout" class="grow" />
<span v-if="position === 'right'" class="ml-2 shrink-0">{{ text }}</span>
</div>
</template>
<script setup lang="ts">
import Divider from 'primevue/divider'
@@ -15,11 +23,3 @@ const {
layout?: 'horizontal' | 'vertical'
}>()
</script>
<template>
<div class="flex items-center">
<span v-if="position === 'left'" class="mr-2 shrink-0">{{ text }}</span>
<Divider :align="align" :type="type" :layout="layout" class="grow" />
<span v-if="position === 'right'" class="ml-2 shrink-0">{{ text }}</span>
</div>
</template>

View File

@@ -1,3 +1,40 @@
<template>
<Tree
v-model:expanded-keys="expandedKeys"
v-model:selection-keys="selectionKeys"
class="tree-explorer py-0 px-2 2xl:px-4"
:class="props.class"
:value="renderedRoot.children"
selection-mode="single"
:pt="{
nodeLabel: 'tree-explorer-node-label',
nodeContent: ({ context }) => ({
class: 'group/tree-node',
onClick: (e: MouseEvent) =>
onNodeContentClick(e, context.node as RenderedTreeExplorerNode),
onContextmenu: (e: MouseEvent) =>
handleContextMenu(e, context.node as RenderedTreeExplorerNode)
}),
nodeToggleButton: () => ({
onClick: (e: MouseEvent) => {
e.stopImmediatePropagation()
}
})
}"
>
<template #folder="{ node }">
<slot name="folder" :node="node">
<TreeExplorerTreeNode :node="node" />
</slot>
</template>
<template #node="{ node }">
<slot name="node" :node="node">
<TreeExplorerTreeNode :node="node" />
</slot>
</template>
</Tree>
<ContextMenu ref="menu" :model="menuItems" />
</template>
<script setup lang="ts">
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem'
@@ -204,43 +241,6 @@ defineExpose({
}
})
</script>
<template>
<Tree
v-model:expanded-keys="expandedKeys"
v-model:selection-keys="selectionKeys"
class="tree-explorer py-0 px-2 2xl:px-4"
:class="props.class"
:value="renderedRoot.children"
selection-mode="single"
:pt="{
nodeLabel: 'tree-explorer-node-label',
nodeContent: ({ context }) => ({
class: 'group/tree-node',
onClick: (e: MouseEvent) =>
onNodeContentClick(e, context.node as RenderedTreeExplorerNode),
onContextmenu: (e: MouseEvent) =>
handleContextMenu(e, context.node as RenderedTreeExplorerNode)
}),
nodeToggleButton: () => ({
onClick: (e: MouseEvent) => {
e.stopImmediatePropagation()
}
})
}"
>
<template #folder="{ node }">
<slot name="folder" :node="node">
<TreeExplorerTreeNode :node="node" />
</slot>
</template>
<template #node="{ node }">
<slot name="node" :node="node">
<TreeExplorerTreeNode :node="node" />
</slot>
</template>
</Tree>
<ContextMenu ref="menu" :model="menuItems" />
</template>
<style scoped>
:deep(.tree-explorer-node-label) {

View File

@@ -1,3 +1,40 @@
<template>
<div
ref="container"
:class="[
'tree-node',
{
'can-drop': canDrop,
'tree-folder': !props.node.leaf,
'tree-leaf': props.node.leaf
}
]"
>
<div class="node-content">
<span class="node-label">
<slot name="before-label" :node="props.node" />
<EditableText
:model-value="node.label"
:is-editing="isEditing"
@edit="handleRename"
/>
<slot name="after-label" :node="props.node" />
</span>
<Badge
v-if="showNodeBadgeText"
:value="nodeBadgeText"
severity="secondary"
class="leaf-count-badge"
/>
</div>
<div
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
>
<slot name="actions" :node="props.node" />
</div>
</div>
</template>
<script setup lang="ts">
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'
import Badge from 'primevue/badge'
@@ -97,43 +134,6 @@ if (props.node.droppable) {
}
</script>
<template>
<div
ref="container"
:class="[
'tree-node',
{
'can-drop': canDrop,
'tree-folder': !props.node.leaf,
'tree-leaf': props.node.leaf
}
]"
>
<div class="node-content">
<span class="node-label">
<slot name="before-label" :node="props.node" />
<EditableText
:model-value="node.label"
:is-editing="isEditing"
@edit="handleRename"
/>
<slot name="after-label" :node="props.node" />
</span>
<Badge
v-if="showNodeBadgeText"
:value="nodeBadgeText"
severity="secondary"
class="leaf-count-badge"
/>
</div>
<div
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
>
<slot name="actions" :node="props.node" />
</div>
</div>
</template>
<style scoped>
.tree-node {
width: 100%;

View File

@@ -1,3 +1,27 @@
<template>
<IconField class="w-full">
<InputText
v-bind="$attrs"
:model-value="internalValue"
class="w-full"
:invalid="validationState === ValidationState.INVALID"
@update:model-value="handleInput"
@blur="handleBlur"
/>
<InputIcon
:class="{
'pi pi-spin pi-spinner text-neutral-400':
validationState === ValidationState.LOADING,
'pi pi-check text-green-500 cursor-pointer':
validationState === ValidationState.VALID,
'pi pi-times text-red-500 cursor-pointer':
validationState === ValidationState.INVALID
}"
@click="validateUrl(props.modelValue)"
/>
</IconField>
</template>
<script setup lang="ts">
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
@@ -103,27 +127,3 @@ defineOptions({
inheritAttrs: false
})
</script>
<template>
<IconField class="w-full">
<InputText
v-bind="$attrs"
:model-value="internalValue"
class="w-full"
:invalid="validationState === ValidationState.INVALID"
@update:model-value="handleInput"
@blur="handleBlur"
/>
<InputIcon
:class="{
'pi pi-spin pi-spinner text-neutral-400':
validationState === ValidationState.LOADING,
'pi pi-check text-green-500 cursor-pointer':
validationState === ValidationState.VALID,
'pi pi-times text-red-500 cursor-pointer':
validationState === ValidationState.INVALID
}"
@click="validateUrl(props.modelValue)"
/>
</IconField>
</template>

View File

@@ -1,3 +1,13 @@
<template>
<Avatar
:image="photoUrl ?? undefined"
:icon="hasAvatar ? undefined : 'pi pi-user'"
shape="circle"
:aria-label="ariaLabel ?? $t('auth.login.userAvatar')"
@error="handleImageError"
/>
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import { computed, ref } from 'vue'
@@ -13,13 +23,3 @@ const handleImageError = () => {
}
const hasAvatar = computed(() => photoUrl && !imageError.value)
</script>
<template>
<Avatar
:image="photoUrl ?? undefined"
:icon="hasAvatar ? undefined : 'pi pi-user'"
shape="circle"
:aria-label="ariaLabel ?? $t('auth.login.userAvatar')"
@error="handleImageError"
/>
</template>

View File

@@ -1,3 +1,22 @@
<template>
<div v-if="balanceLoading" class="flex items-center gap-1">
<div class="flex items-center gap-2">
<Skeleton shape="circle" width="1.5rem" height="1.5rem" />
</div>
<div class="flex-1"></div>
<Skeleton width="8rem" height="2rem" />
</div>
<div v-else class="flex items-center gap-1">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<div :class="textClass">{{ formattedBalance }}</div>
</div>
</template>
<script setup lang="ts">
import Skeleton from 'primevue/skeleton'
import Tag from 'primevue/tag'
@@ -18,22 +37,3 @@ const formattedBalance = computed(() => {
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
})
</script>
<template>
<div v-if="balanceLoading" class="flex items-center gap-1">
<div class="flex items-center gap-2">
<Skeleton shape="circle" width="1.5rem" height="1.5rem" />
</div>
<div class="flex-1"></div>
<Skeleton width="8rem" height="2rem" />
</div>
<div v-else class="flex items-center gap-1">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<div :class="textClass">{{ formattedBalance }}</div>
</div>
</template>

View File

@@ -1,3 +1,19 @@
<template>
<div ref="container" class="scroll-container">
<div :style="{ height: `${(state.start / cols) * itemHeight}px` }" />
<div :style="gridStyle">
<div v-for="item in renderedItems" :key="item.key" data-virtual-grid-item>
<slot name="item" :item="item" />
</div>
</div>
<div
:style="{
height: `${((items.length - state.end) / cols) * itemHeight}px`
}"
/>
</div>
</template>
<script setup lang="ts" generic="T">
import { useElementSize, useScroll, whenever } from '@vueuse/core'
import { clamp, debounce } from 'es-toolkit/compat'
@@ -96,22 +112,6 @@ onBeforeUnmount(() => {
})
</script>
<template>
<div ref="container" class="scroll-container">
<div :style="{ height: `${(state.start / cols) * itemHeight}px` }" />
<div :style="gridStyle">
<div v-for="item in renderedItems" :key="item.key" data-virtual-grid-item>
<slot name="item" :item="item" />
</div>
</div>
<div
:style="{
height: `${((items.length - state.end) / cols) * itemHeight}px`
}"
/>
</div>
</template>
<style scoped>
.scroll-container {
height: 100%;

View File

@@ -1,12 +1,4 @@
<!-- The main global dialog to show various things -->
<script setup lang="ts">
import Dialog from 'primevue/dialog'
import { useDialogStore } from '@/stores/dialogStore'
const dialogStore = useDialogStore()
</script>
<template>
<Dialog
v-for="item in dialogStore.dialogStack"
@@ -42,6 +34,14 @@ const dialogStore = useDialogStore()
</Dialog>
</template>
<script setup lang="ts">
import Dialog from 'primevue/dialog'
import { useDialogStore } from '@/stores/dialogStore'
const dialogStore = useDialogStore()
</script>
<style>
@reference '../../assets/css/style.css';

View File

@@ -1,3 +1,14 @@
<template>
<div>
<!--
UnloadWindowConfirmDialog: This component does not render
anything visible. It is used to confirm the user wants to
close the window, and if they do, it will call the
beforeunload event.
-->
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted } from 'vue'
@@ -26,14 +37,3 @@ onBeforeUnmount(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
})
</script>
<template>
<div>
<!--
UnloadWindowConfirmDialog: This component does not render
anything visible. It is used to confirm the user wants to
close the window, and if they do, it will call the
beforeunload event.
-->
</div>
</template>

View File

@@ -1,20 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const { apiNodeNames, onLogin, onCancel } = defineProps<{
apiNodeNames: string[]
onLogin?: () => void
onCancel?: () => void
}>()
const handleLearnMoreClick = () => {
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
}
</script>
<template>
<div class="flex flex-col gap-4 max-w-96 h-110 p-2">
<div class="text-2xl font-medium mb-2">
@@ -41,3 +24,20 @@ const handleLearnMoreClick = () => {
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const { apiNodeNames, onLogin, onCancel } = defineProps<{
apiNodeNames: string[]
onLogin?: () => void
onCancel?: () => void
}>()
const handleLearnMoreClick = () => {
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
}
</script>

View File

@@ -1,41 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import Message from 'primevue/message'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ConfirmationDialogType } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
const props = defineProps<{
message: string
type: ConfirmationDialogType
onConfirm: (value?: boolean) => void
itemList?: string[]
hint?: string
}>()
const { t } = useI18n()
const onCancel = () => useDialogStore().closeDialog()
const doNotAskAgain = ref(false)
const onDeny = () => {
props.onConfirm(false)
useDialogStore().closeDialog()
}
const onConfirm = () => {
if (props.type === 'overwriteBlueprint' && doNotAskAgain.value)
void useSettingStore().set('Comfy.Workflow.WarnBlueprintOverwrite', false)
props.onConfirm(true)
useDialogStore().closeDialog()
}
</script>
<template>
<section class="prompt-dialog-content flex flex-col gap-6 m-2 mt-4">
<span>{{ message }}</span>
@@ -125,6 +87,44 @@ const onConfirm = () => {
</section>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import Message from 'primevue/message'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ConfirmationDialogType } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
const props = defineProps<{
message: string
type: ConfirmationDialogType
onConfirm: (value?: boolean) => void
itemList?: string[]
hint?: string
}>()
const { t } = useI18n()
const onCancel = () => useDialogStore().closeDialog()
const doNotAskAgain = ref(false)
const onDeny = () => {
props.onConfirm(false)
useDialogStore().closeDialog()
}
const onConfirm = () => {
if (props.type === 'overwriteBlueprint' && doNotAskAgain.value)
void useSettingStore().set('Comfy.Workflow.WarnBlueprintOverwrite', false)
props.onConfirm(true)
useDialogStore().closeDialog()
}
</script>
<style lang="css" scoped>
.prompt-dialog-content {
white-space: pre-wrap;

View File

@@ -1,3 +1,55 @@
<template>
<div class="comfy-error-report flex flex-col gap-4">
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="title"
:message="error.exceptionMessage"
:text-class="'break-words max-w-[60vw]'"
/>
<template v-if="error.extensionFile">
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
<br />
<span class="font-bold">{{ error.extensionFile }}</span>
</template>
<div class="flex gap-2 justify-center">
<Button
v-show="!reportOpen"
text
:label="$t('g.showReport')"
@click="showReport"
/>
<Button
v-show="!reportOpen"
text
:label="$t('issueReport.helpFix')"
@click="showContactSupport"
/>
</div>
<template v-if="reportOpen">
<Divider />
<ScrollPanel class="w-full h-[400px] max-w-[80vw]">
<pre class="whitespace-pre-wrap break-words">{{ reportContent }}</pre>
</ScrollPanel>
<Divider />
</template>
<div class="flex gap-4 justify-end">
<FindIssueButton
:error-message="error.exceptionMessage"
:repo-owner="repoOwner"
:repo-name="repoName"
/>
<Button
v-if="reportOpen"
:label="$t('g.copyToClipboard')"
icon="pi pi-copy"
@click="copyReportToClipboard"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
@@ -85,55 +137,3 @@ const copyReportToClipboard = async () => {
await copyToClipboard(reportContent.value)
}
</script>
<template>
<div class="comfy-error-report flex flex-col gap-4">
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="title"
:message="error.exceptionMessage"
:text-class="'break-words max-w-[60vw]'"
/>
<template v-if="error.extensionFile">
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
<br />
<span class="font-bold">{{ error.extensionFile }}</span>
</template>
<div class="flex gap-2 justify-center">
<Button
v-show="!reportOpen"
text
:label="$t('g.showReport')"
@click="showReport"
/>
<Button
v-show="!reportOpen"
text
:label="$t('issueReport.helpFix')"
@click="showContactSupport"
/>
</div>
<template v-if="reportOpen">
<Divider />
<ScrollPanel class="w-full h-[400px] max-w-[80vw]">
<pre class="whitespace-pre-wrap break-words">{{ reportContent }}</pre>
</ScrollPanel>
<Divider />
</template>
<div class="flex gap-4 justify-end">
<FindIssueButton
:error-message="error.exceptionMessage"
:repo-owner="repoOwner"
:repo-name="repoName"
/>
<Button
v-if="reportOpen"
:label="$t('g.copyToClipboard')"
icon="pi pi-copy"
@click="copyReportToClipboard"
/>
</div>
</div>
</template>

View File

@@ -1,3 +1,55 @@
<template>
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="$t('loadWorkflowWarning.missingNodesTitle')"
:message="$t('loadWorkflowWarning.missingNodesDescription')"
/>
<MissingCoreNodesMessage :missing-core-nodes="missingCoreNodes" />
<ListBox
:options="uniqueNodes"
option-label="label"
scroll-height="100%"
class="comfy-missing-nodes"
:pt="{
list: { class: 'border-none' }
}"
>
<template #option="slotProps">
<div class="flex align-items-center">
<span class="node-type">{{ slotProps.option.label }}</span>
<span v-if="slotProps.option.hint" class="node-hint">{{
slotProps.option.hint
}}</span>
<Button
v-if="slotProps.option.action"
:label="slotProps.option.action.text"
size="small"
outlined
@click="slotProps.option.action.callback"
/>
</div>
</template>
</ListBox>
<div v-if="showManagerButtons" class="flex justify-end py-3">
<PackInstallButton
v-if="showInstallAllButton"
size="md"
:disabled="
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
"
:is-loading="isLoading"
:node-packs="missingNodePacks"
:label="
isLoading
? $t('manager.gettingInfo')
: $t('manager.installAllMissingNodes')
"
/>
<Button label="Open Manager" size="small" outlined @click="openManager" />
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ListBox from 'primevue/listbox'
@@ -102,58 +154,6 @@ watch(allMissingNodesInstalled, async (allInstalled) => {
})
</script>
<template>
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="$t('loadWorkflowWarning.missingNodesTitle')"
:message="$t('loadWorkflowWarning.missingNodesDescription')"
/>
<MissingCoreNodesMessage :missing-core-nodes="missingCoreNodes" />
<ListBox
:options="uniqueNodes"
option-label="label"
scroll-height="100%"
class="comfy-missing-nodes"
:pt="{
list: { class: 'border-none' }
}"
>
<template #option="slotProps">
<div class="flex align-items-center">
<span class="node-type">{{ slotProps.option.label }}</span>
<span v-if="slotProps.option.hint" class="node-hint">{{
slotProps.option.hint
}}</span>
<Button
v-if="slotProps.option.action"
:label="slotProps.option.action.text"
size="small"
outlined
@click="slotProps.option.action.callback"
/>
</div>
</template>
</ListBox>
<div v-if="showManagerButtons" class="flex justify-end py-3">
<PackInstallButton
v-if="showInstallAllButton"
size="md"
:disabled="
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
"
:is-loading="isLoading"
:node-packs="missingNodePacks"
:label="
isLoading
? $t('manager.gettingInfo')
: $t('manager.installAllMissingNodes')
"
/>
<Button label="Open Manager" size="small" outlined @click="openManager" />
</div>
</template>
<style scoped>
.comfy-missing-nodes {
max-height: 300px;

View File

@@ -1,46 +1,3 @@
<script setup lang="ts">
import Message from 'primevue/message'
import { compare } from 'semver'
import { computed } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
const props = defineProps<{
missingCoreNodes: Record<string, LGraphNode[]>
}>()
const systemStatsStore = useSystemStatsStore()
const hasMissingCoreNodes = computed(() => {
return Object.keys(props.missingCoreNodes).length > 0
})
// Use computed for reactive version tracking
const currentComfyUIVersion = computed<string | null>(() => {
if (!hasMissingCoreNodes.value) return null
return systemStatsStore.systemStats?.system?.comfyui_version ?? null
})
const sortedMissingCoreNodes = computed(() => {
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
// Sort by version in descending order (newest first)
return compare(b, a) // Reversed for descending order
})
})
const getUniqueNodeNames = (nodes: LGraphNode[]): string[] => {
return nodes
.reduce<string[]>((acc, node) => {
if (node.type && !acc.includes(node.type)) {
acc.push(node.type)
}
return acc
}, [])
.sort()
}
</script>
<template>
<Message
v-if="hasMissingCoreNodes"
@@ -83,3 +40,46 @@ const getUniqueNodeNames = (nodes: LGraphNode[]): string[] => {
</div>
</Message>
</template>
<script setup lang="ts">
import Message from 'primevue/message'
import { compare } from 'semver'
import { computed } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
const props = defineProps<{
missingCoreNodes: Record<string, LGraphNode[]>
}>()
const systemStatsStore = useSystemStatsStore()
const hasMissingCoreNodes = computed(() => {
return Object.keys(props.missingCoreNodes).length > 0
})
// Use computed for reactive version tracking
const currentComfyUIVersion = computed<string | null>(() => {
if (!hasMissingCoreNodes.value) return null
return systemStatsStore.systemStats?.system?.comfyui_version ?? null
})
const sortedMissingCoreNodes = computed(() => {
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
// Sort by version in descending order (newest first)
return compare(b, a) // Reversed for descending order
})
})
const getUniqueNodeNames = (nodes: LGraphNode[]): string[] => {
return nodes
.reduce<string[]>((acc, node) => {
if (node.type && !acc.includes(node.type)) {
acc.push(node.type)
}
return acc
}, [])
.sort()
}
</script>

View File

@@ -1,3 +1,35 @@
<template>
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="t('missingModelsDialog.missingModels')"
:message="t('missingModelsDialog.missingModelsMessage')"
/>
<div class="flex gap-1 mb-4">
<Checkbox v-model="doNotAskAgain" binary input-id="doNotAskAgain" />
<label for="doNotAskAgain">{{
t('missingModelsDialog.doNotAskAgain')
}}</label>
</div>
<ListBox :options="missingModels" class="comfy-missing-models">
<template #option="{ option }">
<Suspense v-if="isElectron()">
<ElectronFileDownload
:url="option.url"
:label="option.label"
:error="option.error"
/>
</Suspense>
<FileDownload
v-else
:url="option.url"
:label="option.label"
:error="option.error"
/>
</template>
</ListBox>
</template>
<script setup lang="ts">
import Checkbox from 'primevue/checkbox'
import ListBox from 'primevue/listbox'
@@ -108,38 +140,6 @@ onBeforeUnmount(async () => {
})
</script>
<template>
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="t('missingModelsDialog.missingModels')"
:message="t('missingModelsDialog.missingModelsMessage')"
/>
<div class="flex gap-1 mb-4">
<Checkbox v-model="doNotAskAgain" binary input-id="doNotAskAgain" />
<label for="doNotAskAgain">{{
t('missingModelsDialog.doNotAskAgain')
}}</label>
</div>
<ListBox :options="missingModels" class="comfy-missing-models">
<template #option="{ option }">
<Suspense v-if="isElectron()">
<ElectronFileDownload
:url="option.url"
:label="option.label"
:error="option.error"
/>
</Suspense>
<FileDownload
v-else
:url="option.url"
:label="option.label"
:error="option.error"
/>
</template>
</ListBox>
</template>
<style scoped>
.comfy-missing-models {
max-height: 300px;

View File

@@ -1,3 +1,21 @@
<template>
<div class="prompt-dialog-content flex flex-col gap-2 pt-8">
<FloatLabel>
<InputText
ref="inputRef"
v-model="inputValue"
autofocus
@keyup.enter="onConfirm"
@focus="selectAllText"
/>
<label>{{ message }}</label>
</FloatLabel>
<Button @click="onConfirm">
{{ $t('g.confirm') }}
</Button>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import FloatLabel from 'primevue/floatlabel'
@@ -27,21 +45,3 @@ const selectAllText = () => {
inputElement.setSelectionRange(0, inputElement.value.length)
}
</script>
<template>
<div class="prompt-dialog-content flex flex-col gap-2 pt-8">
<FloatLabel>
<InputText
ref="inputRef"
v-model="inputValue"
autofocus
@keyup.enter="onConfirm"
@focus="selectAllText"
/>
<label>{{ message }}</label>
</FloatLabel>
<Button @click="onConfirm">
{{ $t('g.confirm') }}
</Button>
</div>
</template>

View File

@@ -1,68 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import Message from 'primevue/message'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
import { isInChina } from '@/utils/networkUtil'
import ApiKeyForm from './signin/ApiKeyForm.vue'
import SignInForm from './signin/SignInForm.vue'
import SignUpForm from './signin/SignUpForm.vue'
const { onSuccess } = defineProps<{
onSuccess: () => void
}>()
const { t } = useI18n()
const authActions = useFirebaseAuthActions()
const isSecureContext = window.isSecureContext
const isSignIn = ref(true)
const showApiKeyForm = ref(false)
const toggleState = () => {
isSignIn.value = !isSignIn.value
showApiKeyForm.value = false
}
const signInWithGoogle = async () => {
if (await authActions.signInWithGoogle()) {
onSuccess()
}
}
const signInWithGithub = async () => {
if (await authActions.signInWithGithub()) {
onSuccess()
}
}
const signInWithEmail = async (values: SignInData) => {
if (await authActions.signInWithEmail(values.email, values.password)) {
onSuccess()
}
}
const signUpWithEmail = async (values: SignUpData) => {
if (await authActions.signUpWithEmail(values.email, values.password)) {
onSuccess()
}
}
const userIsInChina = ref(false)
onMounted(async () => {
userIsInChina.value = await isInChina()
})
onUnmounted(() => {
authActions.accessError.value = false
})
</script>
<template>
<div class="w-96 p-2 overflow-x-hidden">
<ApiKeyForm
@@ -203,3 +138,68 @@ onUnmounted(() => {
</template>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import Message from 'primevue/message'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
import { isInChina } from '@/utils/networkUtil'
import ApiKeyForm from './signin/ApiKeyForm.vue'
import SignInForm from './signin/SignInForm.vue'
import SignUpForm from './signin/SignUpForm.vue'
const { onSuccess } = defineProps<{
onSuccess: () => void
}>()
const { t } = useI18n()
const authActions = useFirebaseAuthActions()
const isSecureContext = window.isSecureContext
const isSignIn = ref(true)
const showApiKeyForm = ref(false)
const toggleState = () => {
isSignIn.value = !isSignIn.value
showApiKeyForm.value = false
}
const signInWithGoogle = async () => {
if (await authActions.signInWithGoogle()) {
onSuccess()
}
}
const signInWithGithub = async () => {
if (await authActions.signInWithGithub()) {
onSuccess()
}
}
const signInWithEmail = async (values: SignInData) => {
if (await authActions.signInWithEmail(values.email, values.password)) {
onSuccess()
}
}
const signUpWithEmail = async (values: SignUpData) => {
if (await authActions.signUpWithEmail(values.email, values.password)) {
onSuccess()
}
}
const userIsInChina = ref(false)
onMounted(async () => {
userIsInChina.value = await isInChina()
})
onUnmounted(() => {
authActions.accessError.value = false
})
</script>

View File

@@ -1,28 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import UserCredit from '@/components/common/UserCredit.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
const {
isInsufficientCredits = false,
amountOptions = [5, 10, 20, 50],
preselectedAmountOption = 10
} = defineProps<{
isInsufficientCredits?: boolean
amountOptions?: number[]
preselectedAmountOption?: number
}>()
const authActions = useFirebaseAuthActions()
const handleSeeDetails = async () => {
await authActions.accessBillingPortal()
}
</script>
<template>
<div class="flex flex-col w-96 p-2 gap-10">
<div v-if="isInsufficientCredits" class="flex flex-col gap-4">
@@ -71,3 +46,28 @@ const handleSeeDetails = async () => {
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import UserCredit from '@/components/common/UserCredit.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
const {
isInsufficientCredits = false,
amountOptions = [5, 10, 20, 50],
preselectedAmountOption = 10
} = defineProps<{
isInsufficientCredits?: boolean
amountOptions?: number[]
preselectedAmountOption?: number
}>()
const authActions = useFirebaseAuthActions()
const handleSeeDetails = async () => {
await authActions.accessBillingPortal()
}
</script>

View File

@@ -1,3 +1,21 @@
<template>
<Form
class="flex flex-col gap-6 w-96"
:resolver="zodResolver(updatePasswordSchema)"
@submit="onSubmit"
>
<PasswordFields />
<!-- Submit Button -->
<Button
type="submit"
:label="$t('userSettings.updatePassword')"
class="h-10 font-medium mt-4"
:loading="loading"
/>
</Form>
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
@@ -28,21 +46,3 @@ const onSubmit = async (event: FormSubmitEvent) => {
}
}
</script>
<template>
<Form
class="flex flex-col gap-6 w-96"
:resolver="zodResolver(updatePasswordSchema)"
@submit="onSubmit"
>
<PasswordFields />
<!-- Submit Button -->
<Button
type="submit"
:label="$t('userSettings.updatePassword')"
class="h-10 font-medium mt-4"
:loading="loading"
/>
</Form>
</template>

View File

@@ -1,3 +1,36 @@
<template>
<div class="flex items-center gap-2">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<InputNumber
v-if="editable"
v-model="customAmount"
:min="1"
:max="1000"
:step="1"
show-buttons
:allow-empty="false"
:highlight-on-focus="true"
pt:pc-input-text:root="w-24"
@blur="(e: InputNumberBlurEvent) => (customAmount = Number(e.value))"
@input="(e: InputNumberInputEvent) => (customAmount = Number(e.value))"
/>
<span v-else class="text-xl">{{ amount }}</span>
</div>
<ProgressSpinner v-if="loading" class="w-8 h-8" />
<Button
v-else
:severity="preselected ? 'primary' : 'secondary'"
:outlined="!preselected"
:label="$t('credits.topUp.buyNow')"
@click="handleBuyNow"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import InputNumber, {
@@ -40,36 +73,3 @@ onBeforeUnmount(() => {
}
})
</script>
<template>
<div class="flex items-center gap-2">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="text-amber-400 p-1"
/>
<InputNumber
v-if="editable"
v-model="customAmount"
:min="1"
:max="1000"
:step="1"
show-buttons
:allow-empty="false"
:highlight-on-focus="true"
pt:pc-input-text:root="w-24"
@blur="(e: InputNumberBlurEvent) => (customAmount = Number(e.value))"
@input="(e: InputNumberInputEvent) => (customAmount = Number(e.value))"
/>
<span v-else class="text-xl">{{ amount }}</span>
</div>
<ProgressSpinner v-if="loading" class="w-8 h-8" />
<Button
v-else
:severity="preselected ? 'primary' : 'secondary'"
:outlined="!preselected"
:label="$t('credits.topUp.buyNow')"
@click="handleBuyNow"
/>
</template>

View File

@@ -1,3 +1,12 @@
<template>
<Button
:label="$t('g.findIssues')"
severity="secondary"
icon="pi pi-github"
@click="openGitHubIssues"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
@@ -16,12 +25,3 @@ const openGitHubIssues = () => {
window.open(url, '_blank')
}
</script>
<template>
<Button
:label="$t('g.findIssues')"
severity="secondary"
icon="pi pi-github"
@click="openGitHubIssues"
/>
</template>

View File

@@ -1,17 +1,3 @@
<script setup lang="ts">
import Divider from 'primevue/divider'
import Tag from 'primevue/tag'
import SystemStatsPanel from '@/components/common/SystemStatsPanel.vue'
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import PanelTemplate from './PanelTemplate.vue'
const systemStatsStore = useSystemStatsStore()
const aboutPanelStore = useAboutPanelStore()
</script>
<template>
<PanelTemplate value="About" class="about-container">
<h2 class="text-2xl font-bold mb-2">
@@ -44,3 +30,17 @@ const aboutPanelStore = useAboutPanelStore()
/>
</PanelTemplate>
</template>
<script setup lang="ts">
import Divider from 'primevue/divider'
import Tag from 'primevue/tag'
import SystemStatsPanel from '@/components/common/SystemStatsPanel.vue'
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import PanelTemplate from './PanelTemplate.vue'
const systemStatsStore = useSystemStatsStore()
const aboutPanelStore = useAboutPanelStore()
</script>

View File

@@ -1,70 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
import Divider from 'primevue/divider'
import Skeleton from 'primevue/skeleton'
import TabPanel from 'primevue/tabpanel'
import { computed, ref, watch } from 'vue'
import UserCredit from '@/components/common/UserCredit.vue'
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency } from '@/utils/formatUtil'
interface CreditHistoryItemData {
title: string
timestamp: string
amount: number
isPositive: boolean
}
const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const loading = computed(() => authStore.loading)
const balanceLoading = computed(() => authStore.isFetchingBalance)
const usageLogsTableRef = ref<InstanceType<typeof UsageLogsTable> | null>(null)
const formattedLastUpdateTime = computed(() =>
authStore.lastBalanceUpdateTime
? authStore.lastBalanceUpdateTime.toLocaleString()
: ''
)
watch(
() => authStore.lastBalanceUpdateTime,
(newTime, oldTime) => {
if (newTime && newTime !== oldTime && usageLogsTableRef.value) {
usageLogsTableRef.value.refresh()
}
}
)
const handlePurchaseCreditsClick = () => {
dialogService.showTopUpCreditsDialog()
}
const handleCreditsHistoryClick = async () => {
await authActions.accessBillingPortal()
}
const handleMessageSupport = async () => {
await commandStore.execute('Comfy.ContactSupport')
}
const handleFaqClick = () => {
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
}
const creditHistory = ref<CreditHistoryItemData[]>([])
</script>
<template>
<TabPanel value="Credits" class="credits-container h-full">
<div class="flex flex-col h-full">
@@ -170,3 +103,70 @@ const creditHistory = ref<CreditHistoryItemData[]>([])
</div>
</TabPanel>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
import Divider from 'primevue/divider'
import Skeleton from 'primevue/skeleton'
import TabPanel from 'primevue/tabpanel'
import { computed, ref, watch } from 'vue'
import UserCredit from '@/components/common/UserCredit.vue'
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency } from '@/utils/formatUtil'
interface CreditHistoryItemData {
title: string
timestamp: string
amount: number
isPositive: boolean
}
const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const loading = computed(() => authStore.loading)
const balanceLoading = computed(() => authStore.isFetchingBalance)
const usageLogsTableRef = ref<InstanceType<typeof UsageLogsTable> | null>(null)
const formattedLastUpdateTime = computed(() =>
authStore.lastBalanceUpdateTime
? authStore.lastBalanceUpdateTime.toLocaleString()
: ''
)
watch(
() => authStore.lastBalanceUpdateTime,
(newTime, oldTime) => {
if (newTime && newTime !== oldTime && usageLogsTableRef.value) {
usageLogsTableRef.value.refresh()
}
}
)
const handlePurchaseCreditsClick = () => {
dialogService.showTopUpCreditsDialog()
}
const handleCreditsHistoryClick = async () => {
await authActions.accessBillingPortal()
}
const handleMessageSupport = async () => {
await commandStore.execute('Comfy.ContactSupport')
}
const handleFaqClick = () => {
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
}
const creditHistory = ref<CreditHistoryItemData[]>([])
</script>

View File

@@ -1,17 +1,4 @@
<!-- A message that displays the current user -->
<script setup lang="ts">
import Button from 'primevue/button'
import Message from 'primevue/message'
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
const logout = async () => {
await userStore.logout()
window.location.reload()
}
</script>
<template>
<Message
v-if="userStore.isMultiUserServer"
@@ -27,3 +14,16 @@ const logout = async () => {
</div>
</Message>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Message from 'primevue/message'
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
const logout = async () => {
await userStore.logout()
window.location.reload()
}
</script>

View File

@@ -1,3 +1,133 @@
<template>
<PanelTemplate value="Keybinding" class="keybinding-panel">
<template #header>
<SearchBox
v-model="filters['global'].value"
:placeholder="$t('g.searchKeybindings') + '...'"
/>
</template>
<DataTable
v-model:selection="selectedCommandData"
:value="commandsData"
:global-filter-fields="['id', 'label']"
:filters="filters"
selection-mode="single"
striped-rows
:pt="{
header: 'px-0'
}"
@row-dblclick="editKeybinding($event.data)"
>
<Column field="actions" header="">
<template #body="slotProps">
<div class="actions invisible flex flex-row">
<Button
icon="pi pi-pencil"
class="p-button-text"
@click="editKeybinding(slotProps.data)"
/>
<Button
icon="pi pi-replay"
class="p-button-text p-button-warn"
:disabled="
!keybindingStore.isCommandKeybindingModified(slotProps.data.id)
"
@click="resetKeybinding(slotProps.data)"
/>
<Button
icon="pi pi-trash"
class="p-button-text p-button-danger"
:disabled="!slotProps.data.keybinding"
@click="removeKeybinding(slotProps.data)"
/>
</div>
</template>
</Column>
<Column
field="id"
:header="$t('g.command')"
sortable
class="max-w-64 2xl:max-w-full"
>
<template #body="slotProps">
<div
class="overflow-hidden text-ellipsis whitespace-nowrap"
:title="slotProps.data.id"
>
{{ slotProps.data.label }}
</div>
</template>
</Column>
<Column field="keybinding" :header="$t('g.keybinding')">
<template #body="slotProps">
<KeyComboDisplay
v-if="slotProps.data.keybinding"
:key-combo="slotProps.data.keybinding.combo"
:is-modified="
keybindingStore.isCommandKeybindingModified(slotProps.data.id)
"
/>
<span v-else>-</span>
</template>
</Column>
<Column field="source" :header="$t('g.source')">
<template #body="slotProps">
<span class="overflow-hidden text-ellipsis">{{
slotProps.data.source || '-'
}}</span>
</template>
</Column>
</DataTable>
<Dialog
v-model:visible="editDialogVisible"
class="min-w-96"
modal
:header="currentEditingCommand?.label"
@hide="cancelEdit"
>
<div>
<InputText
ref="keybindingInput"
class="mb-2 text-center"
:model-value="newBindingKeyCombo?.toString() ?? ''"
placeholder="Press keys for new binding"
autocomplete="off"
fluid
@keydown.stop.prevent="captureKeybinding"
/>
<Message v-if="existingKeybindingOnCombo" severity="warn">
{{ $t('g.keybindingAlreadyExists') }}
<Tag
severity="secondary"
:value="existingKeybindingOnCombo.commandId"
/>
</Message>
</div>
<template #footer>
<Button
:label="existingKeybindingOnCombo ? 'Overwrite' : 'Save'"
:icon="existingKeybindingOnCombo ? 'pi pi-pencil' : 'pi pi-check'"
:severity="existingKeybindingOnCombo ? 'warn' : undefined"
autofocus
@click="saveKeybinding"
/>
</template>
</Dialog>
<Button
v-tooltip="$t('g.resetAllKeybindingsTooltip')"
class="mt-4"
:label="$t('g.resetAll')"
icon="pi pi-replay"
severity="danger"
fluid
text
@click="resetAllKeybindings"
/>
</PanelTemplate>
</template>
<script setup lang="ts">
import { FilterMatchMode } from '@primevue/core/api'
import Button from 'primevue/button'
@@ -164,136 +294,6 @@ async function resetAllKeybindings() {
}
</script>
<template>
<PanelTemplate value="Keybinding" class="keybinding-panel">
<template #header>
<SearchBox
v-model="filters['global'].value"
:placeholder="$t('g.searchKeybindings') + '...'"
/>
</template>
<DataTable
v-model:selection="selectedCommandData"
:value="commandsData"
:global-filter-fields="['id', 'label']"
:filters="filters"
selection-mode="single"
striped-rows
:pt="{
header: 'px-0'
}"
@row-dblclick="editKeybinding($event.data)"
>
<Column field="actions" header="">
<template #body="slotProps">
<div class="actions invisible flex flex-row">
<Button
icon="pi pi-pencil"
class="p-button-text"
@click="editKeybinding(slotProps.data)"
/>
<Button
icon="pi pi-replay"
class="p-button-text p-button-warn"
:disabled="
!keybindingStore.isCommandKeybindingModified(slotProps.data.id)
"
@click="resetKeybinding(slotProps.data)"
/>
<Button
icon="pi pi-trash"
class="p-button-text p-button-danger"
:disabled="!slotProps.data.keybinding"
@click="removeKeybinding(slotProps.data)"
/>
</div>
</template>
</Column>
<Column
field="id"
:header="$t('g.command')"
sortable
class="max-w-64 2xl:max-w-full"
>
<template #body="slotProps">
<div
class="overflow-hidden text-ellipsis whitespace-nowrap"
:title="slotProps.data.id"
>
{{ slotProps.data.label }}
</div>
</template>
</Column>
<Column field="keybinding" :header="$t('g.keybinding')">
<template #body="slotProps">
<KeyComboDisplay
v-if="slotProps.data.keybinding"
:key-combo="slotProps.data.keybinding.combo"
:is-modified="
keybindingStore.isCommandKeybindingModified(slotProps.data.id)
"
/>
<span v-else>-</span>
</template>
</Column>
<Column field="source" :header="$t('g.source')">
<template #body="slotProps">
<span class="overflow-hidden text-ellipsis">{{
slotProps.data.source || '-'
}}</span>
</template>
</Column>
</DataTable>
<Dialog
v-model:visible="editDialogVisible"
class="min-w-96"
modal
:header="currentEditingCommand?.label"
@hide="cancelEdit"
>
<div>
<InputText
ref="keybindingInput"
class="mb-2 text-center"
:model-value="newBindingKeyCombo?.toString() ?? ''"
placeholder="Press keys for new binding"
autocomplete="off"
fluid
@keydown.stop.prevent="captureKeybinding"
/>
<Message v-if="existingKeybindingOnCombo" severity="warn">
{{ $t('g.keybindingAlreadyExists') }}
<Tag
severity="secondary"
:value="existingKeybindingOnCombo.commandId"
/>
</Message>
</div>
<template #footer>
<Button
:label="existingKeybindingOnCombo ? 'Overwrite' : 'Save'"
:icon="existingKeybindingOnCombo ? 'pi pi-pencil' : 'pi pi-check'"
:severity="existingKeybindingOnCombo ? 'warn' : undefined"
autofocus
@click="saveKeybinding"
/>
</template>
</Dialog>
<Button
v-tooltip="$t('g.resetAllKeybindingsTooltip')"
class="mt-4"
:label="$t('g.resetAll')"
icon="pi pi-replay"
severity="danger"
fluid
text
@click="resetAllKeybindings"
/>
</PanelTemplate>
</template>
<style scoped>
@reference '../../../../assets/css/style.css';

View File

@@ -1,13 +1,3 @@
<script setup lang="ts">
import ScrollPanel from 'primevue/scrollpanel'
import TabPanel from 'primevue/tabpanel'
const props = defineProps<{
value: string
class?: string
}>()
</script>
<template>
<TabPanel :value="props.value" class="h-full w-full" :class="props.class">
<div class="flex flex-col h-full w-full gap-2">
@@ -19,3 +9,13 @@ const props = defineProps<{
</div>
</TabPanel>
</template>
<script setup lang="ts">
import ScrollPanel from 'primevue/scrollpanel'
import TabPanel from 'primevue/tabpanel'
const props = defineProps<{
value: string
class?: string
}>()
</script>

View File

@@ -1,3 +1,92 @@
<template>
<div>
<div v-if="loading" class="flex items-center justify-center p-8">
<ProgressSpinner />
</div>
<div v-else-if="error" class="p-4">
<Message severity="error" :closable="false">{{ error }}</Message>
</div>
<DataTable
v-else
:value="events"
:paginator="true"
:rows="pagination.limit"
:total-records="pagination.total"
:first="dataTableFirst"
:lazy="true"
class="p-datatable-sm custom-datatable"
@page="onPageChange"
>
<Column field="event_type" :header="$t('credits.eventType')">
<template #body="{ data }">
<Badge
:value="customerEventService.formatEventType(data.event_type)"
:severity="customerEventService.getEventSeverity(data.event_type)"
/>
</template>
</Column>
<Column field="details" :header="$t('credits.details')">
<template #body="{ data }">
<div class="event-details">
<!-- Credits Added -->
<template v-if="data.event_type === EventType.CREDIT_ADDED">
<div class="text-green-500 font-semibold">
{{ $t('credits.added') }} ${{
customerEventService.formatAmount(data.params?.amount)
}}
</div>
</template>
<!-- Account Created -->
<template v-else-if="data.event_type === EventType.ACCOUNT_CREATED">
<div>{{ $t('credits.accountInitialized') }}</div>
</template>
<!-- API Usage -->
<template
v-else-if="data.event_type === EventType.API_USAGE_COMPLETED"
>
<div class="flex flex-col gap-1">
<div class="font-semibold">
{{ data.params?.api_name || 'API' }}
</div>
<div class="text-sm text-gray-400">
{{ $t('credits.model') }}: {{ data.params?.model || '-' }}
</div>
</div>
</template>
</div>
</template>
</Column>
<Column field="createdAt" :header="$t('credits.time')">
<template #body="{ data }">
{{ customerEventService.formatDate(data.createdAt) }}
</template>
</Column>
<Column field="params" :header="$t('credits.additionalInfo')">
<template #body="{ data }">
<Button
v-if="customerEventService.hasAdditionalInfo(data)"
v-tooltip.top="{
escape: false,
value: tooltipContentMap.get(data.event_id) || '',
pt: {
text: {
style: {
width: 'max-content !important'
}
}
}
}"
icon="pi pi-info-circle"
class="p-button-text p-button-sm"
/>
</template>
</Column>
</DataTable>
</div>
</template>
<script setup lang="ts">
import Badge from 'primevue/badge'
import Button from 'primevue/button'
@@ -97,92 +186,3 @@ defineExpose({
refresh
})
</script>
<template>
<div>
<div v-if="loading" class="flex items-center justify-center p-8">
<ProgressSpinner />
</div>
<div v-else-if="error" class="p-4">
<Message severity="error" :closable="false">{{ error }}</Message>
</div>
<DataTable
v-else
:value="events"
:paginator="true"
:rows="pagination.limit"
:total-records="pagination.total"
:first="dataTableFirst"
:lazy="true"
class="p-datatable-sm custom-datatable"
@page="onPageChange"
>
<Column field="event_type" :header="$t('credits.eventType')">
<template #body="{ data }">
<Badge
:value="customerEventService.formatEventType(data.event_type)"
:severity="customerEventService.getEventSeverity(data.event_type)"
/>
</template>
</Column>
<Column field="details" :header="$t('credits.details')">
<template #body="{ data }">
<div class="event-details">
<!-- Credits Added -->
<template v-if="data.event_type === EventType.CREDIT_ADDED">
<div class="text-green-500 font-semibold">
{{ $t('credits.added') }} ${{
customerEventService.formatAmount(data.params?.amount)
}}
</div>
</template>
<!-- Account Created -->
<template v-else-if="data.event_type === EventType.ACCOUNT_CREATED">
<div>{{ $t('credits.accountInitialized') }}</div>
</template>
<!-- API Usage -->
<template
v-else-if="data.event_type === EventType.API_USAGE_COMPLETED"
>
<div class="flex flex-col gap-1">
<div class="font-semibold">
{{ data.params?.api_name || 'API' }}
</div>
<div class="text-sm text-gray-400">
{{ $t('credits.model') }}: {{ data.params?.model || '-' }}
</div>
</div>
</template>
</div>
</template>
</Column>
<Column field="createdAt" :header="$t('credits.time')">
<template #body="{ data }">
{{ customerEventService.formatDate(data.createdAt) }}
</template>
</Column>
<Column field="params" :header="$t('credits.additionalInfo')">
<template #body="{ data }">
<Button
v-if="customerEventService.hasAdditionalInfo(data)"
v-tooltip.top="{
escape: false,
value: tooltipContentMap.get(data.event_id) || '',
pt: {
text: {
style: {
width: 'max-content !important'
}
}
}
}"
icon="pi pi-info-circle"
class="p-button-text p-button-sm"
/>
</template>
</Column>
</DataTable>
</div>
</template>

View File

@@ -1,30 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ProgressSpinner from 'primevue/progressspinner'
import TabPanel from 'primevue/tabpanel'
import UserAvatar from '@/components/common/UserAvatar.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useDialogService } from '@/services/dialogService'
const dialogService = useDialogService()
const {
loading,
isLoggedIn,
isApiKeyLogin,
isEmailProvider,
userDisplayName,
userEmail,
userPhotoUrl,
providerName,
providerIcon,
handleSignOut,
handleSignIn,
handleDeleteAccount
} = useCurrentUser()
</script>
<template>
<TabPanel value="User" class="user-settings-container h-full">
<div class="flex flex-col h-full">
@@ -121,3 +94,30 @@ const {
</div>
</TabPanel>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ProgressSpinner from 'primevue/progressspinner'
import TabPanel from 'primevue/tabpanel'
import UserAvatar from '@/components/common/UserAvatar.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useDialogService } from '@/services/dialogService'
const dialogService = useDialogService()
const {
loading,
isLoggedIn,
isApiKeyLogin,
isEmailProvider,
userDisplayName,
userEmail,
userPhotoUrl,
providerName,
providerIcon,
handleSignOut,
handleSignIn,
handleDeleteAccount
} = useCurrentUser()
</script>

View File

@@ -1,3 +1,14 @@
<template>
<span>
<template v-for="(sequence, index) in keySequences" :key="index">
<Tag :severity="isModified ? 'info' : 'secondary'">
{{ sequence }}
</Tag>
<span v-if="index < keySequences.length - 1" class="px-2">+</span>
</template>
</span>
</template>
<script setup lang="ts">
import Tag from 'primevue/tag'
import { computed } from 'vue'
@@ -11,14 +22,3 @@ const { keyCombo, isModified = false } = defineProps<{
const keySequences = computed(() => keyCombo.getKeySequences())
</script>
<template>
<span>
<template v-for="(sequence, index) in keySequences" :key="index">
<Tag :severity="isModified ? 'info' : 'secondary'">
{{ sequence }}
</Tag>
<span v-if="index < keySequences.length - 1" class="px-2">+</span>
</template>
</span>
</template>

View File

@@ -1,37 +1,3 @@
<script setup lang="ts">
import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import { apiKeySchema } from '@/schemas/signInSchema'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore()
const apiKeyStore = useApiKeyAuthStore()
const loading = computed(() => authStore.loading)
const { t } = useI18n()
const emit = defineEmits<{
(e: 'back'): void
(e: 'success'): void
}>()
const onSubmit = async (event: FormSubmitEvent) => {
if (event.valid) {
await apiKeyStore.storeApiKey(event.values.apiKey)
emit('success')
}
}
</script>
<template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-4 mb-8">
@@ -111,3 +77,37 @@ const onSubmit = async (event: FormSubmitEvent) => {
</Form>
</div>
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import { apiKeySchema } from '@/schemas/signInSchema'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore()
const apiKeyStore = useApiKeyAuthStore()
const loading = computed(() => authStore.loading)
const { t } = useI18n()
const emit = defineEmits<{
(e: 'back'): void
(e: 'success'): void
}>()
const onSubmit = async (event: FormSubmitEvent) => {
if (event.valid) {
await apiKeyStore.storeApiKey(event.values.apiKey)
emit('success')
}
}
</script>

View File

@@ -1,23 +1,3 @@
<script setup lang="ts">
import { FormField } from '@primevue/forms'
import Password from 'primevue/password'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const password = ref('')
// TODO: Use dynamic form to better organize the password checks.
// Ref: https://primevue.org/forms/#dynamic
const passwordChecks = computed(() => ({
length: password.value.length >= 8 && password.value.length <= 32,
uppercase: /[A-Z]/.test(password.value),
lowercase: /[a-z]/.test(password.value),
number: /\d/.test(password.value),
special: /[^A-Za-z0-9]/.test(password.value)
}))
</script>
<template>
<!-- Password Field -->
<FormField v-slot="$field" name="password" class="flex flex-col gap-2">
@@ -109,3 +89,23 @@ const passwordChecks = computed(() => ({
}}</small>
</FormField>
</template>
<script setup lang="ts">
import { FormField } from '@primevue/forms'
import Password from 'primevue/password'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const password = ref('')
// TODO: Use dynamic form to better organize the password checks.
// Ref: https://primevue.org/forms/#dynamic
const passwordChecks = computed(() => ({
length: password.value.length >= 8 && password.value.length <= 32,
uppercase: /[A-Z]/.test(password.value),
lowercase: /[a-z]/.test(password.value),
number: /\d/.test(password.value),
special: /[^A-Za-z0-9]/.test(password.value)
}))
</script>

View File

@@ -1,56 +1,3 @@
<script setup lang="ts">
import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import ProgressSpinner from 'primevue/progressspinner'
import { useToast } from 'primevue/usetoast'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore()
const firebaseAuthActions = useFirebaseAuthActions()
const loading = computed(() => authStore.loading)
const toast = useToast()
const { t } = useI18n()
const emit = defineEmits<{
submit: [values: SignInData]
}>()
const emailInputId = 'comfy-org-sign-in-email'
const onSubmit = (event: FormSubmitEvent) => {
if (event.valid) {
emit('submit', event.values as SignInData)
}
}
const handleForgotPassword = async (
email: string,
isValid: boolean | undefined
) => {
if (!email || !isValid) {
toast.add({
severity: 'warn',
summary: t('auth.login.emailPlaceholder'),
life: 5_000
})
// Focus the email input
document.getElementById(emailInputId)?.focus?.()
return
}
await firebaseAuthActions.sendPasswordReset(email)
}
</script>
<template>
<Form
v-slot="$form"
@@ -123,6 +70,59 @@ const handleForgotPassword = async (
</Form>
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import ProgressSpinner from 'primevue/progressspinner'
import { useToast } from 'primevue/usetoast'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore()
const firebaseAuthActions = useFirebaseAuthActions()
const loading = computed(() => authStore.loading)
const toast = useToast()
const { t } = useI18n()
const emit = defineEmits<{
submit: [values: SignInData]
}>()
const emailInputId = 'comfy-org-sign-in-email'
const onSubmit = (event: FormSubmitEvent) => {
if (event.valid) {
emit('submit', event.values as SignInData)
}
}
const handleForgotPassword = async (
email: string,
isValid: boolean | undefined
) => {
if (!email || !isValid) {
toast.add({
severity: 'warn',
summary: t('auth.login.emailPlaceholder'),
life: 5_000
})
// Focus the email input
document.getElementById(emailInputId)?.focus?.()
return
}
await firebaseAuthActions.sendPasswordReset(email)
}
</script>
<style scoped>
@reference '../../../../assets/css/style.css';

View File

@@ -1,29 +1,3 @@
<script setup lang="ts">
import type { FormSubmitEvent } from '@primevue/forms'
import { Form, FormField } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import InputText from 'primevue/inputtext'
import { useI18n } from 'vue-i18n'
import { type SignUpData, signUpSchema } from '@/schemas/signInSchema'
import PasswordFields from './PasswordFields.vue'
const { t } = useI18n()
const emit = defineEmits<{
submit: [values: SignUpData]
}>()
const onSubmit = (event: FormSubmitEvent) => {
if (event.valid) {
emit('submit', event.values as SignUpData)
}
}
</script>
<template>
<Form
class="flex flex-col gap-6"
@@ -83,3 +57,29 @@ const onSubmit = (event: FormSubmitEvent) => {
/>
</Form>
</template>
<script setup lang="ts">
import type { FormSubmitEvent } from '@primevue/forms'
import { Form, FormField } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import InputText from 'primevue/inputtext'
import { useI18n } from 'vue-i18n'
import { type SignUpData, signUpSchema } from '@/schemas/signInSchema'
import PasswordFields from './PasswordFields.vue'
const { t } = useI18n()
const emit = defineEmits<{
submit: [values: SignUpData]
}>()
const onSubmit = (event: FormSubmitEvent) => {
if (event.valid) {
emit('submit', event.values as SignUpData)
}
}
</script>

View File

@@ -1,10 +1,3 @@
<script setup lang="ts">
import Tag from 'primevue/tag'
// Global variable from vite build defined in global.d.ts
// eslint-disable-next-line no-undef
const isStaging = !__USE_PROD_CONFIG__
</script>
<template>
<div>
<h2 class="px-4">
@@ -19,6 +12,13 @@ const isStaging = !__USE_PROD_CONFIG__
</h2>
</div>
</template>
<script setup lang="ts">
import Tag from 'primevue/tag'
// Global variable from vite build defined in global.d.ts
// eslint-disable-next-line no-undef
const isStaging = !__USE_PROD_CONFIG__
</script>
<style scoped>
.pi-cog {

View File

@@ -1,3 +1,15 @@
<template>
<!-- Create a new stacking context for widgets to avoid z-index issues -->
<div class="isolate">
<DomWidget
v-for="widgetState in widgetStates"
:key="widgetState.widget.id"
:widget-state="widgetState"
@update:widget-value="widgetState.widget.value = $event"
/>
</div>
</template>
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import { computed } from 'vue'
@@ -61,15 +73,3 @@ whenever(
{ immediate: true }
)
</script>
<template>
<!-- Create a new stacking context for widgets to avoid z-index issues -->
<div class="isolate">
<DomWidget
v-for="widgetState in widgetStates"
:key="widgetState.widget.id"
:widget-state="widgetState"
@update:widget-value="widgetState.widget.value = $event"
/>
</div>
</template>

View File

@@ -1,3 +1,74 @@
<template>
<!-- Load splitter overlay only after comfyApp is ready. -->
<!-- If load immediately, the top-level splitter stateKey won't be correctly
synced with the stateStorage (localStorage). -->
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled">
<template v-if="!workspaceStore.focusMode" #side-bar-panel>
<SideToolbar />
</template>
<template v-if="!workspaceStore.focusMode" #bottom-panel>
<BottomPanel />
</template>
<template #graph-canvas-panel>
<div class="absolute top-0 left-0 w-auto max-w-full pointer-events-auto">
<SecondRowWorkflowTabs
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
/>
</div>
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
<MiniMap
v-if="comfyAppReady && minimapEnabled"
class="pointer-events-auto"
/>
</template>
</LiteGraphCanvasSplitterOverlay>
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
<canvas
id="graph-canvas"
ref="canvasRef"
tabindex="1"
class="align-top w-full h-full touch-none"
/>
<!-- TransformPane for Vue node rendering -->
<TransformPane
v-if="shouldRenderVueNodes && comfyApp.canvas && comfyAppReady"
:canvas="comfyApp.canvas"
@transform-update="handleTransformUpdate"
@wheel.capture="canvasInteractions.forwardEventToCanvas"
>
<!-- Vue nodes rendered based on graph nodes -->
<VueGraphNode
v-for="nodeData in allNodes"
:key="nodeData.id"
:node-data="nodeData"
:position="nodePositions.get(nodeData.id)"
:size="nodeSizes.get(nodeData.id)"
:readonly="false"
:error="
executionStore.lastExecutionError?.node_id === nodeData.id
? 'Execution error'
: null
"
:zoom-level="canvasStore.canvas?.ds?.scale || 1"
:data-node-id="nodeData.id"
/>
</TransformPane>
<NodeTooltip v-if="tooltipEnabled" />
<NodeSearchboxPopover ref="nodeSearchboxPopoverRef" />
<!-- Initialize components after comfyApp is ready. useAbsolutePosition requires
canvasStore.canvas to be initialized. -->
<template v-if="comfyAppReady">
<TitleEditor />
<SelectionToolbox v-if="selectionToolboxEnabled" />
<!-- Render legacy DOM widgets only when Vue nodes are disabled -->
<DomWidgets v-if="!shouldRenderVueNodes" />
</template>
</template>
<script setup lang="ts">
import { useEventListener, whenever } from '@vueuse/core'
import {
@@ -384,74 +455,3 @@ onUnmounted(() => {
vueNodeLifecycle.cleanup()
})
</script>
<template>
<!-- Load splitter overlay only after comfyApp is ready. -->
<!-- If load immediately, the top-level splitter stateKey won't be correctly
synced with the stateStorage (localStorage). -->
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled">
<template v-if="!workspaceStore.focusMode" #side-bar-panel>
<SideToolbar />
</template>
<template v-if="!workspaceStore.focusMode" #bottom-panel>
<BottomPanel />
</template>
<template #graph-canvas-panel>
<div class="absolute top-0 left-0 w-auto max-w-full pointer-events-auto">
<SecondRowWorkflowTabs
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
/>
</div>
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
<MiniMap
v-if="comfyAppReady && minimapEnabled"
class="pointer-events-auto"
/>
</template>
</LiteGraphCanvasSplitterOverlay>
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
<canvas
id="graph-canvas"
ref="canvasRef"
tabindex="1"
class="align-top w-full h-full touch-none"
/>
<!-- TransformPane for Vue node rendering -->
<TransformPane
v-if="shouldRenderVueNodes && comfyApp.canvas && comfyAppReady"
:canvas="comfyApp.canvas"
@transform-update="handleTransformUpdate"
@wheel.capture="canvasInteractions.forwardEventToCanvas"
>
<!-- Vue nodes rendered based on graph nodes -->
<VueGraphNode
v-for="nodeData in allNodes"
:key="nodeData.id"
:node-data="nodeData"
:position="nodePositions.get(nodeData.id)"
:size="nodeSizes.get(nodeData.id)"
:readonly="false"
:error="
executionStore.lastExecutionError?.node_id === nodeData.id
? 'Execution error'
: null
"
:zoom-level="canvasStore.canvas?.ds?.scale || 1"
:data-node-id="nodeData.id"
/>
</TransformPane>
<NodeTooltip v-if="tooltipEnabled" />
<NodeSearchboxPopover ref="nodeSearchboxPopoverRef" />
<!-- Initialize components after comfyApp is ready. useAbsolutePosition requires
canvasStore.canvas to be initialized. -->
<template v-if="comfyAppReady">
<TitleEditor />
<SelectionToolbox v-if="selectionToolboxEnabled" />
<!-- Render legacy DOM widgets only when Vue nodes are disabled -->
<DomWidgets v-if="!shouldRenderVueNodes" />
</template>
</template>

View File

@@ -1,3 +1,123 @@
<template>
<div>
<ZoomControlsModal :visible="isModalVisible" />
<!-- Backdrop -->
<div
v-if="hasActivePopup"
class="fixed inset-0 z-1200"
@click="hideModal"
></div>
<ButtonGroup
class="p-buttongroup-vertical p-1 absolute bottom-4 right-2 md:right-4"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
>
<Button
v-tooltip.top="selectTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
severity="secondary"
:aria-label="selectTooltip"
:pressed="isCanvasReadOnly"
icon="i-material-symbols:pan-tool-outline"
:class="selectButtonClass"
@click="() => commandStore.execute('Comfy.Canvas.Unlock')"
>
<template #icon>
<i-lucide:mouse-pointer-2 />
</template>
</Button>
<Button
v-tooltip.top="handTooltip"
severity="secondary"
:aria-label="handTooltip"
:pressed="isCanvasUnlocked"
:class="handButtonClass"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.Lock')"
>
<template #icon>
<i-lucide:hand />
</template>
</Button>
<!-- vertical line with bg E1DED5 -->
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
<Button
v-tooltip.top="fitViewTooltip"
severity="secondary"
icon="pi pi-expand"
:aria-label="fitViewTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
class="dark-theme:hover:bg-[#444444]! hover:bg-[#E7E6E6]!"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
>
<template #icon>
<i-lucide:focus />
</template>
</Button>
<Button
ref="zoomButton"
v-tooltip.top="t('zoomControls.label')"
severity="secondary"
:label="t('zoomControls.label')"
:class="zoomButtonClass"
:aria-label="t('zoomControls.label')"
data-testid="zoom-controls-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="toggleModal"
>
<span class="inline-flex text-xs">
<span>{{ canvasStore.appScalePercentage }}%</span>
<i-lucide:chevron-down />
</span>
</Button>
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
<Button
ref="focusButton"
v-tooltip.top="focusModeTooltip"
severity="secondary"
:aria-label="focusModeTooltip"
data-testid="focus-mode-button"
:style="stringifiedMinimapStyles.buttonStyles"
:class="focusButtonClass"
@click="() => commandStore.execute('Workspace.ToggleFocusMode')"
>
<template #icon>
<i-lucide:lightbulb />
</template>
</Button>
<Button
v-tooltip.top="{
value: linkVisibilityTooltip,
pt: {
root: {
style: 'z-index: 2; transform: translateY(-20px);'
}
}
}"
severity="secondary"
:class="linkVisibleClass"
:aria-label="linkVisibilityAriaLabel"
data-testid="toggle-link-visibility-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
>
<template #icon>
<i-lucide:route-off />
</template>
</Button>
</ButtonGroup>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ButtonGroup from 'primevue/buttongroup'
@@ -148,126 +268,6 @@ onBeforeUnmount(() => {
})
</script>
<template>
<div>
<ZoomControlsModal :visible="isModalVisible" />
<!-- Backdrop -->
<div
v-if="hasActivePopup"
class="fixed inset-0 z-1200"
@click="hideModal"
></div>
<ButtonGroup
class="p-buttongroup-vertical p-1 absolute bottom-4 right-2 md:right-4"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
>
<Button
v-tooltip.top="selectTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
severity="secondary"
:aria-label="selectTooltip"
:pressed="isCanvasReadOnly"
icon="i-material-symbols:pan-tool-outline"
:class="selectButtonClass"
@click="() => commandStore.execute('Comfy.Canvas.Unlock')"
>
<template #icon>
<i-lucide:mouse-pointer-2 />
</template>
</Button>
<Button
v-tooltip.top="handTooltip"
severity="secondary"
:aria-label="handTooltip"
:pressed="isCanvasUnlocked"
:class="handButtonClass"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.Lock')"
>
<template #icon>
<i-lucide:hand />
</template>
</Button>
<!-- vertical line with bg E1DED5 -->
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
<Button
v-tooltip.top="fitViewTooltip"
severity="secondary"
icon="pi pi-expand"
:aria-label="fitViewTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
class="dark-theme:hover:bg-[#444444]! hover:bg-[#E7E6E6]!"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
>
<template #icon>
<i-lucide:focus />
</template>
</Button>
<Button
ref="zoomButton"
v-tooltip.top="t('zoomControls.label')"
severity="secondary"
:label="t('zoomControls.label')"
:class="zoomButtonClass"
:aria-label="t('zoomControls.label')"
data-testid="zoom-controls-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="toggleModal"
>
<span class="inline-flex text-xs">
<span>{{ canvasStore.appScalePercentage }}%</span>
<i-lucide:chevron-down />
</span>
</Button>
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
<Button
ref="focusButton"
v-tooltip.top="focusModeTooltip"
severity="secondary"
:aria-label="focusModeTooltip"
data-testid="focus-mode-button"
:style="stringifiedMinimapStyles.buttonStyles"
:class="focusButtonClass"
@click="() => commandStore.execute('Workspace.ToggleFocusMode')"
>
<template #icon>
<i-lucide:lightbulb />
</template>
</Button>
<Button
v-tooltip.top="{
value: linkVisibilityTooltip,
pt: {
root: {
style: 'z-index: 2; transform: translateY(-20px);'
}
}
}"
severity="secondary"
:class="linkVisibleClass"
:aria-label="linkVisibilityAriaLabel"
data-testid="toggle-link-visibility-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
>
<template #icon>
<i-lucide:route-off />
</template>
</Button>
</ButtonGroup>
</div>
</template>
<style scoped>
.p-buttongroup-vertical {
display: flex;

View File

@@ -1,3 +1,14 @@
<template>
<div
v-if="tooltipText"
ref="tooltipRef"
class="node-tooltip"
:style="{ left, top }"
>
{{ tooltipText }}
</div>
</template>
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { nextTick, ref } from 'vue'
@@ -118,17 +129,6 @@ useEventListener(window, 'mousemove', onMouseMove)
useEventListener(window, 'click', hideTooltip)
</script>
<template>
<div
v-if="tooltipText"
ref="tooltipRef"
class="node-tooltip"
:style="{ left, top }"
>
{{ tooltipText }}
</div>
</template>
<style lang="css" scoped>
.node-tooltip {
pointer-events: none;

View File

@@ -1,3 +1,49 @@
<template>
<div
ref="toolboxRef"
style="transform: translate(var(--tb-x), var(--tb-y))"
class="fixed left-0 top-0 z-40 pointer-events-none"
>
<Transition name="slide-up">
<Panel
v-if="visible"
class="rounded-lg selection-toolbox pointer-events-auto"
:style="`backgroundColor: ${containerStyles.backgroundColor};`"
:pt="{
header: 'hidden',
content: 'p-1 h-10 flex flex-row gap-1'
}"
@wheel="canvasInteractions.handleWheel"
>
<DeleteButton v-if="showDelete" />
<VerticalDivider v-if="showInfoButton && showAnyPrimaryActions" />
<InfoButton v-if="showInfoButton" />
<ColorPickerButton v-if="showColorPicker" />
<FrameNodes v-if="showFrameNodes" />
<ConvertToSubgraphButton v-if="showConvertToSubgraph" />
<PublishSubgraphButton v-if="showPublishSubgraph" />
<MaskEditorButton v-if="showMaskEditor" />
<VerticalDivider
v-if="showAnyPrimaryActions && showAnyControlActions"
/>
<BypassButton v-if="showBypass" />
<RefreshSelectionButton v-if="showRefresh" />
<Load3DViewerButton v-if="showLoad3DViewer" />
<ExtensionCommandButton
v-for="command in extensionToolboxCommands"
:key="command.id"
:command="command"
/>
<ExecuteButton v-if="showExecute" />
<MoreOptions />
</Panel>
</Transition>
</div>
</template>
<script setup lang="ts">
import Panel from 'primevue/panel'
import { computed, ref } from 'vue'
@@ -90,52 +136,6 @@ const showAnyPrimaryActions = computed(
const showAnyControlActions = computed(() => showBypass.value)
</script>
<template>
<div
ref="toolboxRef"
style="transform: translate(var(--tb-x), var(--tb-y))"
class="fixed left-0 top-0 z-40 pointer-events-none"
>
<Transition name="slide-up">
<Panel
v-if="visible"
class="rounded-lg selection-toolbox pointer-events-auto"
:style="`backgroundColor: ${containerStyles.backgroundColor};`"
:pt="{
header: 'hidden',
content: 'p-1 h-10 flex flex-row gap-1'
}"
@wheel="canvasInteractions.handleWheel"
>
<DeleteButton v-if="showDelete" />
<VerticalDivider v-if="showInfoButton && showAnyPrimaryActions" />
<InfoButton v-if="showInfoButton" />
<ColorPickerButton v-if="showColorPicker" />
<FrameNodes v-if="showFrameNodes" />
<ConvertToSubgraphButton v-if="showConvertToSubgraph" />
<PublishSubgraphButton v-if="showPublishSubgraph" />
<MaskEditorButton v-if="showMaskEditor" />
<VerticalDivider
v-if="showAnyPrimaryActions && showAnyControlActions"
/>
<BypassButton v-if="showBypass" />
<RefreshSelectionButton v-if="showRefresh" />
<Load3DViewerButton v-if="showLoad3DViewer" />
<ExtensionCommandButton
v-for="command in extensionToolboxCommands"
:key="command.id"
:command="command"
/>
<ExecuteButton v-if="showExecute" />
<MoreOptions />
</Panel>
</Transition>
</div>
</template>
<style scoped>
.selection-toolbox {
transform: translateX(-50%) translateY(-120%);

View File

@@ -1,3 +1,17 @@
<template>
<div
v-if="showInput"
class="group-title-editor node-title-editor"
:style="inputStyle"
>
<EditableText
:is-editing="showInput"
:model-value="editedTitle"
@edit="onEdit"
/>
</div>
</template>
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { type CSSProperties, computed, ref, watch } from 'vue'
@@ -117,20 +131,6 @@ const canvasEventHandler = (event: LiteGraphCanvasEvent) => {
useEventListener(document, 'litegraph:canvas', canvasEventHandler)
</script>
<template>
<div
v-if="showInput"
class="group-title-editor node-title-editor"
:style="inputStyle"
>
<EditableText
:is-editing="showInput"
:model-value="editedTitle"
@edit="onEdit"
/>
</div>
</template>
<style scoped>
.group-title-editor.node-title-editor {
z-index: 9999;

View File

@@ -1,97 +1,3 @@
<script setup lang="ts">
import type { InputNumberInputEvent } from 'primevue'
import { Button, InputNumber } from 'primevue'
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
const { t } = useI18n()
const minimap = useMinimap()
const settingStore = useSettingStore()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const { formatKeySequence } = useCommandStore()
interface Props {
visible: boolean
}
const props = defineProps<Props>()
const interval = ref<number | null>(null)
// Computed properties for reactive states
const minimapToggleText = computed(() =>
settingStore.get('Comfy.Minimap.Visible')
? t('zoomControls.hideMinimap')
: t('zoomControls.showMinimap')
)
const applyZoom = (val: InputNumberInputEvent) => {
const inputValue = val.value as number
if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {
return
}
canvasStore.setAppZoomFromPercentage(inputValue)
}
const executeCommand = (command: string) => {
void commandStore.execute(command)
}
const startRepeat = (command: string) => {
if (interval.value) return
const cmd = () => commandStore.execute(command)
void cmd()
interval.value = window.setInterval(cmd, 100)
}
const stopRepeat = () => {
if (interval.value) {
clearInterval(interval.value)
interval.value = null
}
}
const filteredMinimapStyles = computed(() => {
return {
...minimap.containerStyles.value,
height: undefined,
width: undefined
}
})
const zoomInCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomIn'))
)
const zoomOutCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomOut'))
)
const zoomToFitCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.FitView'))
)
const showMinimapCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ToggleMinimap'))
)
const zoomInput = ref<InstanceType<typeof InputNumber> | null>(null)
const zoomInputContainer = ref<HTMLDivElement | null>(null)
watch(
() => props.visible,
async (newVal) => {
if (newVal) {
await nextTick()
const input = zoomInputContainer.value?.querySelector(
'input'
) as HTMLInputElement
input?.focus()
}
}
)
</script>
<template>
<div
v-if="visible"
@@ -227,6 +133,100 @@ watch(
</div>
</div>
</template>
<script setup lang="ts">
import type { InputNumberInputEvent } from 'primevue'
import { Button, InputNumber } from 'primevue'
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
const { t } = useI18n()
const minimap = useMinimap()
const settingStore = useSettingStore()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const { formatKeySequence } = useCommandStore()
interface Props {
visible: boolean
}
const props = defineProps<Props>()
const interval = ref<number | null>(null)
// Computed properties for reactive states
const minimapToggleText = computed(() =>
settingStore.get('Comfy.Minimap.Visible')
? t('zoomControls.hideMinimap')
: t('zoomControls.showMinimap')
)
const applyZoom = (val: InputNumberInputEvent) => {
const inputValue = val.value as number
if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {
return
}
canvasStore.setAppZoomFromPercentage(inputValue)
}
const executeCommand = (command: string) => {
void commandStore.execute(command)
}
const startRepeat = (command: string) => {
if (interval.value) return
const cmd = () => commandStore.execute(command)
void cmd()
interval.value = window.setInterval(cmd, 100)
}
const stopRepeat = () => {
if (interval.value) {
clearInterval(interval.value)
interval.value = null
}
}
const filteredMinimapStyles = computed(() => {
return {
...minimap.containerStyles.value,
height: undefined,
width: undefined
}
})
const zoomInCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomIn'))
)
const zoomOutCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomOut'))
)
const zoomToFitCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.FitView'))
)
const showMinimapCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ToggleMinimap'))
)
const zoomInput = ref<InstanceType<typeof InputNumber> | null>(null)
const zoomInputContainer = ref<HTMLDivElement | null>(null)
watch(
() => props.visible,
async (newVal) => {
if (newVal) {
await nextTick()
const input = zoomInputContainer.value?.querySelector(
'input'
) as HTMLInputElement
input?.focus()
}
}
)
</script>
<style>
.zoomInputContainer:focus-within {
border: 1px solid rgb(204, 204, 204);

View File

@@ -1,17 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
const { t } = useI18n()
const commandStore = useCommandStore()
const toggleBypass = async () => {
await commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
}
</script>
<template>
<Button
v-tooltip.top="{
@@ -29,3 +15,17 @@ const toggleBypass = async () => {
</template>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
const { t } = useI18n()
const commandStore = useCommandStore()
const toggleBypass = async () => {
await commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
}
</script>

View File

@@ -1,3 +1,52 @@
<template>
<div class="relative">
<Button
v-tooltip.top="{
value: localizedCurrentColorName ?? t('color.noColor'),
showDelay: 1000
}"
data-testid="color-picker-button"
severity="secondary"
text
@click="() => (showColorPicker = !showColorPicker)"
>
<div class="flex items-center gap-1 px-0">
<i
class="w-4 h-4 pi pi-circle-fill"
:style="{ color: currentColor ?? '' }"
/>
<i
class="w-4 h-4 pi pi-chevron-down py-1"
:style="{ fontSize: '0.5rem' }"
/>
</div>
</Button>
<div
v-if="showColorPicker"
class="color-picker-container absolute -top-10 left-1/2"
>
<SelectButton
:model-value="selectedColorOption"
:options="colorOptions"
option-label="name"
data-key="value"
@update:model-value="applyColor"
>
<template #option="{ option }">
<i
v-tooltip.top="option.localizedName"
class="pi pi-circle-fill"
:style="{
color: isLightTheme ? option.value.light : option.value.dark
}"
:data-testid="option.name"
/>
</template>
</SelectButton>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import SelectButton from 'primevue/selectbutton'
@@ -115,55 +164,6 @@ watch(
)
</script>
<template>
<div class="relative">
<Button
v-tooltip.top="{
value: localizedCurrentColorName ?? t('color.noColor'),
showDelay: 1000
}"
data-testid="color-picker-button"
severity="secondary"
text
@click="() => (showColorPicker = !showColorPicker)"
>
<div class="flex items-center gap-1 px-0">
<i
class="w-4 h-4 pi pi-circle-fill"
:style="{ color: currentColor ?? '' }"
/>
<i
class="w-4 h-4 pi pi-chevron-down py-1"
:style="{ fontSize: '0.5rem' }"
/>
</div>
</Button>
<div
v-if="showColorPicker"
class="color-picker-container absolute -top-10 left-1/2"
>
<SelectButton
:model-value="selectedColorOption"
:options="colorOptions"
option-label="name"
data-key="value"
@update:model-value="applyColor"
>
<template #option="{ option }">
<i
v-tooltip.top="option.localizedName"
class="pi pi-circle-fill"
:style="{
color: isLightTheme ? option.value.light : option.value.dark
}"
:data-testid="option.name"
/>
</template>
</SelectButton>
</div>
</div>
</template>
<style scoped>
@reference '../../../assets/css/style.css';

View File

@@ -1,21 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSelectionState } from '@/composables/graph/useSelectionState'
import { useCommandStore } from '@/stores/commandStore'
const { t } = useI18n()
const commandStore = useCommandStore()
const { isSingleSubgraph, hasAnySelection } = useSelectionState()
const isUnpackVisible = isSingleSubgraph
const isConvertVisible = computed(
() => hasAnySelection.value && !isSingleSubgraph.value
)
</script>
<template>
<Button
v-if="isUnpackVisible"
@@ -48,3 +30,21 @@ const isConvertVisible = computed(
</template>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSelectionState } from '@/composables/graph/useSelectionState'
import { useCommandStore } from '@/stores/commandStore'
const { t } = useI18n()
const commandStore = useCommandStore()
const { isSingleSubgraph, hasAnySelection } = useSelectionState()
const isUnpackVisible = isSingleSubgraph
const isConvertVisible = computed(
() => hasAnySelection.value && !isSingleSubgraph.value
)
</script>

View File

@@ -1,3 +1,19 @@
<template>
<Button
v-show="isDeletable"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
showDelay: 1000
}"
severity="secondary"
text
icon-class="w-4 h-4"
icon="pi pi-trash"
data-testid="delete-button"
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
@@ -15,19 +31,3 @@ const isDeletable = computed(() =>
selectedItems.value.some((x: Positionable) => x.removable !== false)
)
</script>
<template>
<Button
v-show="isDeletable"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
showDelay: 1000
}"
severity="secondary"
text
icon-class="w-4 h-4"
icon="pi pi-trash"
data-testid="delete-button"
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
/>
</template>

View File

@@ -1,3 +1,19 @@
<template>
<Button
v-tooltip.top="{
value: t('selectionToolbox.executeButton.tooltip'),
showDelay: 1000
}"
class="dark-theme:bg-[#0B8CE9] bg-[#31B9F4] size-8 !p-0"
text
@mouseenter="() => handleMouseEnter()"
@mouseleave="() => handleMouseLeave()"
@click="handleClick"
>
<i-lucide:play class="fill-path-white w-4 h-4" />
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed, ref } from 'vue'
@@ -48,22 +64,6 @@ const handleClick = async () => {
await commandStore.execute('Comfy.QueueSelectedOutputNodes')
}
</script>
<template>
<Button
v-tooltip.top="{
value: t('selectionToolbox.executeButton.tooltip'),
showDelay: 1000
}"
class="dark-theme:bg-[#0B8CE9] bg-[#31B9F4] size-8 !p-0"
text
@mouseenter="() => handleMouseEnter()"
@mouseleave="() => handleMouseLeave()"
@click="handleClick"
>
<i-lucide:play class="fill-path-white w-4 h-4" />
</Button>
</template>
<style scoped>
:deep.fill-path-white > path {
fill: white;

View File

@@ -1,18 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { st } from '@/i18n'
import type { ComfyCommand } from '@/stores/commandStore'
import { useCommandStore } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
defineProps<{
command: ComfyCommand
}>()
const commandStore = useCommandStore()
</script>
<template>
<Button
v-tooltip.top="{
@@ -27,3 +12,18 @@ const commandStore = useCommandStore()
@click="() => commandStore.execute(command.id)"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { st } from '@/i18n'
import type { ComfyCommand } from '@/stores/commandStore'
import { useCommandStore } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
defineProps<{
command: ComfyCommand
}>()
const commandStore = useCommandStore()
</script>

View File

@@ -1,11 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { useFrameNodes } from '@/composables/graph/useFrameNodes'
const { frameNodes } = useFrameNodes()
</script>
<template>
<Button
v-tooltip.top="{
@@ -20,3 +12,11 @@ const { frameNodes } = useFrameNodes()
<i-lucide:frame class="w-4 h-4" />
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useFrameNodes } from '@/composables/graph/useFrameNodes'
const { frameNodes } = useFrameNodes()
</script>

View File

@@ -1,11 +1,3 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { useSelectionState } from '@/composables/graph/useSelectionState'
const { showNodeHelp: toggleHelp } = useSelectionState()
</script>
<template>
<Button
v-tooltip.top="{
@@ -20,3 +12,11 @@ const { showNodeHelp: toggleHelp } = useSelectionState()
<i-lucide:info class="w-4 h-4" />
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useSelectionState } from '@/composables/graph/useSelectionState'
const { showNodeHelp: toggleHelp } = useSelectionState()
</script>

Some files were not shown because too many files have changed in this diff Show More