Compare commits
1 Commits
drjkl/desl
...
fix/subgra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7faf9eafde |
3
.gitignore
vendored
@@ -98,5 +98,4 @@ vitest.config.*.timestamp*
|
||||
# Weekly docs check output
|
||||
/output.txt
|
||||
|
||||
.amp
|
||||
.venv
|
||||
.amp
|
||||
@@ -1,7 +1,19 @@
|
||||
export interface UVMirror {
|
||||
/**
|
||||
* The setting id defined for the mirror.
|
||||
*/
|
||||
settingId: string
|
||||
/**
|
||||
* The default mirror to use.
|
||||
*/
|
||||
mirror: string
|
||||
/**
|
||||
* The fallback mirror to use.
|
||||
*/
|
||||
fallbackMirror: string
|
||||
/**
|
||||
* The path suffix to validate the mirror is reachable.
|
||||
*/
|
||||
validationPathSuffix?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
import type { PrimeVueSeverity } from '../primeVueTypes'
|
||||
|
||||
interface MaintenanceTaskButton {
|
||||
/** The text to display on the button. */
|
||||
text?: string
|
||||
/** CSS classes, e.g. 'pi pi-external-link' */
|
||||
/** CSS classes used for the button icon, e.g. 'pi pi-external-link' */
|
||||
icon?: string
|
||||
}
|
||||
|
||||
/** A maintenance task, used by the maintenance page. */
|
||||
export interface MaintenanceTask {
|
||||
/** Used as i18n key */
|
||||
/** ID string used as i18n key */
|
||||
id: string
|
||||
/** The display name of the task, e.g. Git */
|
||||
name: string
|
||||
/** Short description of the task. */
|
||||
shortDescription?: string
|
||||
/** Description of the task when it is in an error state. */
|
||||
errorDescription?: string
|
||||
/** Description of the task when it is in a warning state. */
|
||||
warningDescription?: string
|
||||
/** Full description of the task when it is in an OK state. */
|
||||
description?: string
|
||||
/** URL to the image to show in card mode. */
|
||||
headerImg?: string
|
||||
/** The button to display on the task card / list item. */
|
||||
button?: MaintenanceTaskButton
|
||||
/** Whether to show a confirmation dialog before running the task. */
|
||||
requireConfirm?: boolean
|
||||
/** The text to display in the confirmation dialog. */
|
||||
confirmText?: string
|
||||
/** Called by onClick to run the actual task. */
|
||||
execute: (args?: unknown[]) => boolean | Promise<boolean>
|
||||
/** Show the button with `severity="danger"` */
|
||||
severity?: PrimeVueSeverity
|
||||
/** Whether this task should display the terminal window when run. */
|
||||
usesTerminal?: boolean
|
||||
/** If true, successful completion refreshes install validation and auto-continues. */
|
||||
/** If `true`, successful completion of this task will refresh install validation and automatically continue if successful. */
|
||||
isInstallationFix?: boolean
|
||||
}
|
||||
|
||||
/** The filter options for the maintenance task list. */
|
||||
export interface MaintenanceFilter {
|
||||
/** CSS classes, e.g. 'pi pi-cross' */
|
||||
/** CSS classes used for the filter button icon, e.g. 'pi pi-cross' */
|
||||
icon: string
|
||||
/** The text to display on the filter button. */
|
||||
value: string
|
||||
/** The tasks to display when this filter is selected. */
|
||||
tasks: ReadonlyArray<MaintenanceTask>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import { isValidUrl } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
import { electronAPI } from './envUtil'
|
||||
|
||||
/**
|
||||
* Check if a mirror is reachable from the electron App.
|
||||
* @param mirror - The mirror to check.
|
||||
* @returns True if the mirror is reachable, false otherwise.
|
||||
*/
|
||||
export const checkMirrorReachable = async (mirror: string) => {
|
||||
return (
|
||||
isValidUrl(mirror) && (await electronAPI().NetWork.canAccessUrl(mirror))
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
type ElectronWindow = typeof window & {
|
||||
electronAPI?: ElectronAPI
|
||||
}
|
||||
|
||||
export function isElectron() {
|
||||
return 'electronAPI' in window && window.electronAPI !== undefined
|
||||
}
|
||||
|
||||
export function electronAPI(): ElectronAPI {
|
||||
return (window as ElectronWindow).electronAPI as ElectronAPI
|
||||
export function electronAPI() {
|
||||
return (window as any).electronAPI as ElectronAPI
|
||||
}
|
||||
|
||||
export function isNativeWindow() {
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function getPromotedWidgetCount(
|
||||
return promotedWidgets.length
|
||||
}
|
||||
|
||||
export function getPromotedWidgetCountByName(
|
||||
export async function getPromotedWidgetCountByName(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: string,
|
||||
widgetName: string
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
@@ -819,13 +819,16 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
const activeWorkflowName =
|
||||
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
|
||||
const workflowPathA = `${workflowA}.json`
|
||||
const workflowPathB = `${workflowB}.json`
|
||||
|
||||
expect(openWorkflows).toEqual(
|
||||
expect.arrayContaining([workflowA, workflowB])
|
||||
expect.arrayContaining([workflowPathA, workflowPathB])
|
||||
)
|
||||
expect(openWorkflows.indexOf(workflowA)).toBeLessThan(
|
||||
openWorkflows.indexOf(workflowB)
|
||||
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
|
||||
openWorkflows.indexOf(workflowPathB)
|
||||
)
|
||||
expect(activeWorkflowName).toEqual(workflowB)
|
||||
expect(activeWorkflowName).toEqual(workflowPathB)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -35,21 +35,18 @@ test.describe(
|
||||
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const shouldUpload = filesWithUpload.has(fileName)
|
||||
const uploadRequestPromise = shouldUpload
|
||||
? comfyPage.page.waitForRequest((req) =>
|
||||
req.url().includes('/upload/')
|
||||
)
|
||||
: null
|
||||
|
||||
await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`)
|
||||
|
||||
if (uploadRequestPromise) {
|
||||
const request = await uploadRequestPromise
|
||||
expect(request.url()).toContain('/upload/')
|
||||
} else {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||
const waitForUpload = filesWithUpload.has(fileName)
|
||||
await comfyPage.dragDrop.dragAndDropFile(
|
||||
`workflowInMedia/${fileName}`,
|
||||
{ waitForUpload }
|
||||
)
|
||||
if (waitForUpload) {
|
||||
await comfyPage.page.waitForResponse(
|
||||
(resp) => resp.url().includes('/view') && resp.status() !== 0,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
}
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
|
||||
})
|
||||
|
||||
test('loads from inserted workflow', async ({ comfyPage }) => {
|
||||
const workflowName = 'single_connected_reroute_node'
|
||||
const workflowName = 'single_connected_reroute_node.json'
|
||||
await comfyPage.workflow.setupWorkflowsDirectory({
|
||||
[`${workflowName}.json`]: `links/${workflowName}.json`
|
||||
[workflowName]: 'links/single_connected_reroute_node.json'
|
||||
})
|
||||
await comfyPage.setup()
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
|
||||
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 90 KiB |
@@ -21,12 +21,14 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
test('Can create new blank workflow', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow',
|
||||
'*Unsaved Workflow (2)'
|
||||
'*Unsaved Workflow.json',
|
||||
'*Unsaved Workflow (2).json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -39,37 +41,37 @@ test.describe('Workflows sidebar', () => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||
expect.arrayContaining(['workflow1', 'workflow2'])
|
||||
expect.arrayContaining(['workflow1.json', 'workflow2.json'])
|
||||
)
|
||||
})
|
||||
|
||||
test('Can duplicate workflow', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1')
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
|
||||
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||
expect.arrayContaining(['workflow1'])
|
||||
expect.arrayContaining(['workflow1.json'])
|
||||
)
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1',
|
||||
'*workflow1 (Copy)'
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json'
|
||||
])
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1',
|
||||
'*workflow1 (Copy)',
|
||||
'*workflow1 (Copy) (2)'
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json',
|
||||
'*workflow1 (Copy) (2).json'
|
||||
])
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1',
|
||||
'*workflow1 (Copy)',
|
||||
'*workflow1 (Copy) (2)',
|
||||
'*workflow1 (Copy) (3)'
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json',
|
||||
'*workflow1 (Copy) (2).json',
|
||||
'*workflow1 (Copy) (3).json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -83,12 +85,12 @@ test.describe('Workflows sidebar', () => {
|
||||
await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow')
|
||||
const originalNodeCount = await comfyPage.nodeOps.getNodeCount()
|
||||
|
||||
await tab.insertWorkflow(tab.getPersistedItem('workflow1'))
|
||||
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getNodeCount())
|
||||
.toEqual(originalNodeCount + 1)
|
||||
|
||||
await tab.getPersistedItem('workflow1').click()
|
||||
await tab.getPersistedItem('workflow1.json').click()
|
||||
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toEqual(1)
|
||||
})
|
||||
|
||||
@@ -111,22 +113,22 @@ test.describe('Workflows sidebar', () => {
|
||||
const openedWorkflow = tab.getOpenedItem('foo/bar')
|
||||
await tab.renameWorkflow(openedWorkflow, 'foo/baz')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow',
|
||||
'foo/baz'
|
||||
'*Unsaved Workflow.json',
|
||||
'foo/baz.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can save workflow as', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow3')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow', 'workflow3'])
|
||||
.toEqual(['*Unsaved Workflow.json', 'workflow3.json'])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow4')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json')
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow', 'workflow3', 'workflow4'])
|
||||
.toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
|
||||
})
|
||||
|
||||
test('Exported workflow does not contain localized slot names', async ({
|
||||
@@ -182,15 +184,15 @@ test.describe('Workflows sidebar', () => {
|
||||
})
|
||||
|
||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5')
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow5'
|
||||
'workflow5.json'
|
||||
])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow5'
|
||||
'workflow5.json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -210,25 +212,25 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
test('Can overwrite other workflows with save as', async ({ comfyPage }) => {
|
||||
const topbar = comfyPage.menu.topbar
|
||||
await topbar.saveWorkflow('workflow1')
|
||||
await topbar.saveWorkflowAs('workflow2')
|
||||
await topbar.saveWorkflow('workflow1.json')
|
||||
await topbar.saveWorkflowAs('workflow2.json')
|
||||
await comfyPage.nextFrame()
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['workflow1', 'workflow2'])
|
||||
.toEqual(['workflow1.json', 'workflow2.json'])
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
||||
.toEqual('workflow2')
|
||||
.toEqual('workflow2.json')
|
||||
|
||||
await topbar.saveWorkflowAs('workflow1')
|
||||
await topbar.saveWorkflowAs('workflow1.json')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
// The old workflow1 should be deleted and the new one should be saved.
|
||||
// The old workflow1.json should be deleted and the new one should be saved.
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['workflow2', 'workflow1'])
|
||||
.toEqual(['workflow2.json', 'workflow1.json'])
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
||||
.toEqual('workflow1')
|
||||
.toEqual('workflow1.json')
|
||||
})
|
||||
|
||||
test('Does not report warning when switching between opened workflows', async ({
|
||||
@@ -264,15 +266,17 @@ test.describe('Workflows sidebar', () => {
|
||||
)
|
||||
await closeButton.click()
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow'
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can close saved workflow with command', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1')
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
|
||||
await comfyPage.command.executeCommand('Workspace.CloseWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
|
||||
@@ -280,7 +284,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18'
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
@@ -291,14 +295,14 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow'
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can delete workflows', async ({ comfyPage }) => {
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18'
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
@@ -310,7 +314,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow'
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -322,11 +326,13 @@ test.describe('Workflows sidebar', () => {
|
||||
const { workflowsTab } = comfyPage.menu
|
||||
await workflowsTab.open()
|
||||
|
||||
await workflowsTab.getPersistedItem('workflow1').click({ button: 'right' })
|
||||
await workflowsTab
|
||||
.getPersistedItem('workflow1.json')
|
||||
.click({ button: 'right' })
|
||||
await comfyPage.contextMenu.clickMenuItem('Duplicate')
|
||||
await expect
|
||||
.poll(() => workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow', '*workflow1 (Copy)'])
|
||||
.toEqual(['*Unsaved Workflow.json', '*workflow1 (Copy).json'])
|
||||
})
|
||||
|
||||
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
|
||||
@@ -338,7 +344,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
// Wait for workflow to appear in Browse section after sync
|
||||
const workflowItem =
|
||||
comfyPage.menu.workflowsTab.getPersistedItem('workflow1')
|
||||
comfyPage.menu.workflowsTab.getPersistedItem('workflow1.json')
|
||||
await expect(workflowItem).toBeVisible({ timeout: 3000 })
|
||||
|
||||
const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
@@ -355,7 +361,7 @@ test.describe('Workflows sidebar', () => {
|
||||
}
|
||||
|
||||
await comfyPage.page.dragAndDrop(
|
||||
'.comfyui-workflows-browse .node-label:has-text("workflow1")',
|
||||
'.comfyui-workflows-browse .node-label:has-text("workflow1.json")',
|
||||
'#graph-canvas',
|
||||
{ targetPosition }
|
||||
)
|
||||
|
||||
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 106 KiB |
@@ -89,11 +89,13 @@
|
||||
"chart.js": "^4.5.0",
|
||||
"cva": "catalog:",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "catalog:",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "catalog:",
|
||||
"fuse.js": "^7.0.0",
|
||||
"glob": "catalog:",
|
||||
"jsonata": "catalog:",
|
||||
"jsondiffpatch": "catalog:",
|
||||
"loglevel": "^1.9.2",
|
||||
@@ -143,7 +145,6 @@
|
||||
"@vue/test-utils": "catalog:",
|
||||
"@webgpu/types": "catalog:",
|
||||
"cross-env": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-import-resolver-typescript": "catalog:",
|
||||
@@ -154,7 +155,6 @@
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
"eslint-plugin-vue": "catalog:",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "catalog:",
|
||||
"globals": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
"husky": "catalog:",
|
||||
|
||||
71
pnpm-lock.yaml
generated
@@ -482,6 +482,9 @@ importers:
|
||||
dompurify:
|
||||
specifier: ^3.2.5
|
||||
version: 3.3.1
|
||||
dotenv:
|
||||
specifier: 'catalog:'
|
||||
version: 16.6.1
|
||||
es-toolkit:
|
||||
specifier: ^1.39.9
|
||||
version: 1.39.10
|
||||
@@ -497,6 +500,9 @@ importers:
|
||||
fuse.js:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
glob:
|
||||
specifier: 'catalog:'
|
||||
version: 13.0.6
|
||||
jsonata:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.0
|
||||
@@ -639,9 +645,6 @@ importers:
|
||||
cross-env:
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.0
|
||||
dotenv:
|
||||
specifier: 'catalog:'
|
||||
version: 16.6.1
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 9.39.1(jiti@2.6.1)
|
||||
@@ -672,9 +675,6 @@ importers:
|
||||
fs-extra:
|
||||
specifier: ^11.2.0
|
||||
version: 11.3.2
|
||||
glob:
|
||||
specifier: 'catalog:'
|
||||
version: 13.0.6
|
||||
globals:
|
||||
specifier: 'catalog:'
|
||||
version: 16.5.0
|
||||
@@ -2451,25 +2451,21 @@ packages:
|
||||
resolution: {integrity: sha512-D+tPXB0tkSuDPsuXvyQIsF3f3PBWfAwIe9FkBWtVoDVYqE+jbz+tVGsjQMNWGafLE4sC8ZQdjhsxyT8I53Anbw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@nx/nx-linux-arm64-musl@22.5.2':
|
||||
resolution: {integrity: sha512-UbO527qqa8KLBi13uXto5SmxcZv1Smer7sPexJonshDlmrJsyvx5m8nm6tcSv04W5yQEL90vPlTux8dNvEDWrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@nx/nx-linux-x64-gnu@22.5.2':
|
||||
resolution: {integrity: sha512-wR6596Vr/Z+blUAmjLHG2TCQMs4O1oi9JXK1J/PoPeO9UqdHwStCJBAd61zDFSUYJe0x+dkeRQu96fE5BW8Kcg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@nx/nx-linux-x64-musl@22.5.2':
|
||||
resolution: {integrity: sha512-MBXOw4AH4FWl4orwVykj/e75awTNDePogrl3pXNX9NcQLdj6JzS4e2jaALQeRBQLxQzeFvFQV/W4PBzoPV6/NA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@nx/nx-win32-arm64-msvc@22.5.2':
|
||||
resolution: {integrity: sha512-SaWSZkRH5uV8vP2lj6RRv+kw2IzaIDXkutReOXpooshIWZl9KjrQELNTCZTYyhLDsMlcyhSvLFlTiA4NkZ8udw==}
|
||||
@@ -2635,49 +2631,41 @@ packages:
|
||||
resolution: {integrity: sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.15.0':
|
||||
resolution: {integrity: sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.15.0':
|
||||
resolution: {integrity: sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.15.0':
|
||||
resolution: {integrity: sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.15.0':
|
||||
resolution: {integrity: sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw==}
|
||||
@@ -2751,56 +2739,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.34.0':
|
||||
resolution: {integrity: sha512-H+F8+71gHQoGTFPPJ6z4dD0Fzfzi0UP8Zx94h5kUmIFThLvMq5K1Y/bUUubiXwwHfwb5C3MPjUpYijiy0rj51Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-dIGnzTNhCXqQD5pzBwduLg8pClm+t8R53qaE9i5h8iua1iaFAJyLffh4847CNZSlASb7gn1Ofuv7KoG/EpoGZg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-FGQ2GTTooilDte/ogwWwkHuuL3lGtcE3uKM2EcC7kOXNWdUfMY6Jx3JCodNVVbFoybv4A+HuCj8WJji2uu1Ceg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.34.0':
|
||||
resolution: {integrity: sha512-2dGbGneJ7ptOIVKMwEIHdCkdZEomh74X3ggo4hCzEXL/rl9HwfsZDR15MkqfQqAs6nVXMvtGIOMxjDYa5lwKaA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-cCtGgmrTrxq3OeSG0UAO+w6yLZTMeOF4XM9SAkNrRUxYhRQELSDQ/iNPCLyHhYNi38uHJQbS5RQweLUDpI4ajA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-7AvMzmeX+k7GdgitXp99GQoIV/QZIpAS7rwxQvC/T541yWC45nwvk4mpnU8N+V6dE5SPEObnqfhCjO80s7qIsg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.34.0':
|
||||
resolution: {integrity: sha512-uNiglhcmivJo1oDMh3hoN/Z0WsbEXOpRXZdQ3W/IkOpyV8WF308jFjSC1ZxajdcNRXWej0zgge9QXba58Owt+g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.34.0':
|
||||
resolution: {integrity: sha512-5eFsTjCyji25j6zznzlMc+wQAZJoL9oWy576xhqd2efv+N4g1swIzuSDcb1dz4gpcVC6veWe9pAwD7HnrGjLwg==}
|
||||
@@ -2903,56 +2883,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.49.0':
|
||||
resolution: {integrity: sha512-xeqkMOARgGBlEg9BQuPDf6ZW711X6BT5qjDyeM5XNowCJeTSdmMhpePJjTEiVbbr3t21sIlK8RE6X5bc04nWyQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-uvcqRO6PnlJGbL7TeePhTK5+7/JXbxGbN+C6FVmfICDeeRomgQqrfVjf0lUrVpUU8ii8TSkIbNdft3M+oNlOsQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-Dw1HkdXAwHNH+ZDserHP2RzXQmhHtpsYYI0hf8fuGAVCIVwvS6w1+InLxpPMY25P8ASRNiFN3hADtoh6lI+4lg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.49.0':
|
||||
resolution: {integrity: sha512-EPlMYaA05tJ9km/0dI9K57iuMq3Tw+nHst7TNIegAJZrBPtsOtYaMFZEaWj02HA8FI5QvSnRHMt+CI+RIhXJBQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-yZiQL9qEwse34aMbnMb5VqiAWfDY+fLFuoJbHOuzB1OaJZbN1MRF9Nk+W89PIpGr5DNPDipwjZb8+Q7wOywoUQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-CcCDwMMXSchNkhdgvhVn3DLZ4EnBXAD8o8+gRzahg+IdSt/72y19xBgShJgadIRF0TsRcV/MhDUMwL5N/W54aQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.49.0':
|
||||
resolution: {integrity: sha512-u3HfKV8BV6t6UCCbN0RRiyqcymhrnpunVmLFI8sEa5S/EBu+p/0bJ3D7LZ2KT6PsBbrB71SWq4DeFrskOVgIZg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.49.0':
|
||||
resolution: {integrity: sha512-dRDpH9fw+oeUMpM4br0taYCFpW6jQtOuEIec89rOgDA1YhqwmeRcx0XYeCv7U48p57qJ1XZHeMGM9LdItIjfzA==}
|
||||
@@ -3121,28 +3093,24 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-Z03/wrqau9Bicfgb3Dbs6SYTHliELk2PM2LpG2nFd+cGupTMF5kanLEcj2vuuJLLhptNyS61rtk7SOZ+lPsTUA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-qaj+MFudtdCv9xZo9znFvkgoajLdc+vwf0Kz5N44g+LU5XMe+IsACgn3UG7uTRlCCvhMAGXm1XlpEA5bZBrOcw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-U662UnMETyjT65gFmG9ma+XziENrs7BBnENi/27swZPYagubfHRirXHG2oMl+pEax2WvO7Kb9gHZmMakpYqBHQ==}
|
||||
@@ -3220,67 +3188,56 @@ packages:
|
||||
resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.53.5':
|
||||
resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.53.5':
|
||||
resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==}
|
||||
@@ -3556,28 +3513,24 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.0':
|
||||
resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.0':
|
||||
resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==}
|
||||
@@ -4053,49 +4006,41 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -6471,28 +6416,24 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
|
||||
|
||||
@@ -133,7 +133,7 @@ function isKeyUsed(key: string, sourceFiles: string[]): boolean {
|
||||
}
|
||||
|
||||
// Main function
|
||||
function checkNewUnusedKeys() {
|
||||
async function checkNewUnusedKeys() {
|
||||
const stagedLocaleFiles = getStagedLocaleFiles()
|
||||
|
||||
if (stagedLocaleFiles.length === 0) {
|
||||
@@ -165,7 +165,7 @@ function checkNewUnusedKeys() {
|
||||
if (unusedNewKeys.length > 0) {
|
||||
console.warn('\n⚠️ Warning: Found unused NEW i18n keys:\n')
|
||||
|
||||
for (const key of unusedNewKeys.sort((a, b) => a.localeCompare(b))) {
|
||||
for (const key of unusedNewKeys.sort()) {
|
||||
console.warn(` - ${key}`)
|
||||
}
|
||||
|
||||
@@ -183,9 +183,7 @@ function checkNewUnusedKeys() {
|
||||
}
|
||||
|
||||
// Run the check
|
||||
try {
|
||||
checkNewUnusedKeys()
|
||||
} catch (err) {
|
||||
checkNewUnusedKeys().catch((err) => {
|
||||
console.error('Error checking unused keys:', err)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -132,7 +132,7 @@ function resolveRelease(
|
||||
return null
|
||||
}
|
||||
|
||||
const [, currentMinor] = currentVersion.split('.').map(Number)
|
||||
const [major, currentMinor, patch] = currentVersion.split('.').map(Number)
|
||||
|
||||
// Fetch all branches
|
||||
exec('git fetch origin', frontendRepoPath)
|
||||
@@ -264,7 +264,7 @@ if (!releaseInfo) {
|
||||
}
|
||||
|
||||
// Output as JSON for GitHub Actions
|
||||
// oxlint-disable-next-line no-console -- CI script output
|
||||
|
||||
console.log(JSON.stringify(releaseInfo, null, 2))
|
||||
|
||||
export { resolveRelease }
|
||||
|
||||
10
src/App.vue
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<GlobalDialog />
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="pointer-events-none fixed inset-0 z-1200 flex items-center justify-center"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<LogoComfyWaveLoader size="xl" color="yellow" />
|
||||
<Loader size="lg" class="text-white" />
|
||||
</div>
|
||||
<GlobalDialog />
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -15,7 +15,7 @@ import { captureException } from '@sentry/vue'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import LogoComfyWaveLoader from '@/components/loader/LogoComfyWaveLoader.vue'
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
@@ -51,10 +51,10 @@ export function downloadFile(url: string, filename?: string): void {
|
||||
|
||||
/**
|
||||
* Download a Blob by creating a temporary object URL and anchor element
|
||||
* @param blob - The Blob to download
|
||||
* @param filename - The filename to suggest to the browser
|
||||
* @param blob - The Blob to download
|
||||
*/
|
||||
export function downloadBlob(blob: Blob, filename: string): void {
|
||||
export function downloadBlob(filename: string, blob: Blob): void {
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
triggerLinkDownload(url, filename)
|
||||
@@ -138,7 +138,7 @@ async function downloadViaBlobFetch(
|
||||
extractFilenameFromContentDisposition(contentDisposition)
|
||||
|
||||
const blob = await response.blob()
|
||||
downloadBlob(blob, headerFilename ?? fallbackFilename)
|
||||
downloadBlob(headerFilename ?? fallbackFilename, blob)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,19 +45,19 @@ export const usdToCredits = (usd: number): number =>
|
||||
export const creditsToUsd = (credits: number): number =>
|
||||
Math.round((credits / CREDITS_PER_USD) * 100) / 100
|
||||
|
||||
type FormatOptions = {
|
||||
export type FormatOptions = {
|
||||
value: number
|
||||
locale?: string
|
||||
numberOptions?: Intl.NumberFormatOptions
|
||||
}
|
||||
|
||||
type FormatFromCentsOptions = {
|
||||
export type FormatFromCentsOptions = {
|
||||
cents: number
|
||||
locale?: string
|
||||
numberOptions?: Intl.NumberFormatOptions
|
||||
}
|
||||
|
||||
type FormatFromUsdOptions = {
|
||||
export type FormatFromUsdOptions = {
|
||||
usd: number
|
||||
locale?: string
|
||||
numberOptions?: Intl.NumberFormatOptions
|
||||
@@ -113,3 +113,13 @@ export const formatUsdFromCents = ({
|
||||
locale,
|
||||
numberOptions
|
||||
})
|
||||
|
||||
/**
|
||||
* Clamps a USD value to the allowed range for credit purchases
|
||||
* @param value - The USD amount to clamp
|
||||
* @returns The clamped value between $1 and $1000, or 0 if NaN
|
||||
*/
|
||||
export const clampUsd = (value: number): number => {
|
||||
if (Number.isNaN(value)) return 0
|
||||
return Math.min(1000, Math.max(1, value))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* Utilities for pointer event handling
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if a pointer or mouse event is a middle button input
|
||||
* @param event - The pointer or mouse event to check
|
||||
* @returns true if the event is from the middle button/wheel
|
||||
*/
|
||||
export function isMiddlePointerInput(
|
||||
event: PointerEvent | MouseEvent
|
||||
): boolean {
|
||||
|
||||
@@ -141,7 +141,7 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
useDraggable,
|
||||
useEventListener,
|
||||
@@ -108,7 +108,7 @@ import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
@@ -3,16 +3,9 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import {
|
||||
openShareDialog,
|
||||
prefetchShareDialog
|
||||
} from '@/platform/workflow/sharing/composables/lazyShareDialog'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
@@ -25,8 +18,6 @@ const workspaceStore = useWorkspaceStore()
|
||||
const { enableAppBuilder } = useAppMode()
|
||||
const appModeStore = useAppModeStore()
|
||||
const { enterBuilder } = appModeStore
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
const { flags } = useFeatureFlags()
|
||||
const { hasNodes } = storeToRefs(appModeStore)
|
||||
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
|
||||
|
||||
@@ -52,7 +43,28 @@ function openTemplates() {
|
||||
|
||||
<template>
|
||||
<div class="pointer-events-auto flex flex-col gap-2">
|
||||
<WorkflowActionsDropdown source="app_mode_toolbar" />
|
||||
<WorkflowActionsDropdown source="app_mode_toolbar">
|
||||
<template #button="{ hasUnseenItems }">
|
||||
<Button
|
||||
v-tooltip.right="{
|
||||
value: t('sideToolbar.labels.menu'),
|
||||
...tooltipOptions
|
||||
}"
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:aria-label="t('sideToolbar.labels.menu')"
|
||||
class="relative h-10 gap-1 rounded-lg pr-2 pl-3 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
|
||||
>
|
||||
<i class="icon-[lucide--panels-top-left] size-4" />
|
||||
<i class="icon-[lucide--chevron-down] size-4 text-muted-foreground" />
|
||||
<span
|
||||
v-if="hasUnseenItems"
|
||||
aria-hidden="true"
|
||||
class="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-primary-background"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</WorkflowActionsDropdown>
|
||||
|
||||
<Button
|
||||
v-if="enableAppBuilder"
|
||||
@@ -69,21 +81,6 @@ function openTemplates() {
|
||||
>
|
||||
<i class="icon-[lucide--hammer] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="isCloud && flags.workflowSharingEnabled"
|
||||
v-tooltip.right="{
|
||||
value: t('actionbar.shareTooltip'),
|
||||
...tooltipOptions
|
||||
}"
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:aria-label="t('actionbar.shareTooltip')"
|
||||
class="size-10 rounded-lg"
|
||||
@click="() => openShareDialog().catch(toastErrorHandler)"
|
||||
@pointerenter="prefetchShareDialog"
|
||||
>
|
||||
<i class="icon-[lucide--send] size-4" />
|
||||
</Button>
|
||||
|
||||
<div
|
||||
class="flex w-10 flex-col overflow-hidden rounded-lg bg-secondary-background"
|
||||
|
||||
@@ -41,7 +41,7 @@ const mockCommands: ComfyCommandImpl[] = [
|
||||
icon: 'pi pi-test',
|
||||
tooltip: 'Test tooltip',
|
||||
menubarLabel: 'Other Command',
|
||||
keybinding: undefined
|
||||
keybinding: null
|
||||
} as ComfyCommandImpl
|
||||
]
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('ShortcutsList', () => {
|
||||
id: 'No.Keybinding',
|
||||
label: 'No Keybinding',
|
||||
category: 'essentials',
|
||||
keybinding: undefined
|
||||
keybinding: null
|
||||
} as ComfyCommandImpl
|
||||
]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
data-testid="subgraph-breadcrumb"
|
||||
class="subgraph-breadcrumb -mt-3 flex w-auto items-center pt-4 pl-1 drop-shadow-(--interface-panel-drop-shadow)"
|
||||
class="subgraph-breadcrumb -mt-4 flex w-auto items-center pt-4 drop-shadow-(--interface-panel-drop-shadow)"
|
||||
:class="{
|
||||
'subgraph-breadcrumb-collapse': collapseTabs,
|
||||
'subgraph-breadcrumb-overflow': overflowingTabs
|
||||
|
||||
@@ -12,10 +12,7 @@ import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
TitleMode
|
||||
} from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -162,8 +159,7 @@ function handleDown(e: MouseEvent) {
|
||||
}
|
||||
function handleClick(e: MouseEvent) {
|
||||
const [node, widget] = getHovered(e) ?? []
|
||||
if (node?.mode !== LGraphEventMode.ALWAYS)
|
||||
return canvasInteractions.forwardEventToCanvas(e)
|
||||
if (!node) return canvasInteractions.forwardEventToCanvas(e)
|
||||
|
||||
if (!widget) {
|
||||
if (!isSelectOutputsMode.value) return
|
||||
@@ -196,10 +192,7 @@ function nodeToDisplayTuple(
|
||||
const renderedOutputs = computed(() => {
|
||||
void appModeStore.selectedOutputs.length
|
||||
return canvas
|
||||
.graph!.nodes.filter(
|
||||
(n) =>
|
||||
n.constructor.nodeData?.output_node && n.mode === LGraphEventMode.ALWAYS
|
||||
)
|
||||
.graph!.nodes.filter((n) => n.constructor.nodeData?.output_node)
|
||||
.map(nodeToDisplayTuple)
|
||||
})
|
||||
const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
@@ -211,146 +204,131 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex items-center border-b border-border-subtle p-2 font-bold">
|
||||
{{
|
||||
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
|
||||
}}
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<DraggableList
|
||||
v-if="isArrangeMode"
|
||||
v-slot="{ dragClass }"
|
||||
v-model="appModeStore.selectedInputs"
|
||||
class="overflow-x-clip"
|
||||
>
|
||||
<div
|
||||
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
|
||||
:key="`${nodeId}: ${widgetName}`"
|
||||
:class="cn(dragClass, 'pointer-events-auto my-2 p-2')"
|
||||
:aria-label="`${widget?.label ?? widgetName} — ${node.title}`"
|
||||
>
|
||||
<div v-if="widget" class="pointer-events-none" inert>
|
||||
<WidgetItem
|
||||
:widget="widget"
|
||||
:node="node"
|
||||
show-node-name
|
||||
hidden-widget-actions
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="pointer-events-none p-1 text-sm text-muted-foreground"
|
||||
>
|
||||
{{ widgetName }}
|
||||
<p class="text-xs italic">
|
||||
({{ t('linearMode.builder.unknownWidget') }})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DraggableList>
|
||||
<PropertiesAccordionItem
|
||||
v-if="isSelectInputsMode"
|
||||
:label="t('nodeHelpPage.inputs')"
|
||||
enable-empty-state
|
||||
:disabled="!appModeStore.selectedInputs.length"
|
||||
class="border-b border-border-subtle"
|
||||
:tooltip="`${t('linearMode.builder.inputsDesc')}\n${t('linearMode.builder.inputsExample')}`"
|
||||
:tooltip-delay="100"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
{{ t('nodeHelpPage.inputs') }}
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('linearMode.builder.promptAddInputs')"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('linearMode.builder.promptAddInputs')"
|
||||
/>
|
||||
<DraggableList
|
||||
v-slot="{ dragClass }"
|
||||
v-model="appModeStore.selectedInputs"
|
||||
>
|
||||
<IoItem
|
||||
v-for="{
|
||||
nodeId,
|
||||
widgetName,
|
||||
label,
|
||||
subLabel,
|
||||
rename
|
||||
} in inputsWithState"
|
||||
:key="`${nodeId}: ${widgetName}`"
|
||||
:class="
|
||||
cn(dragClass, 'my-2 rounded-lg bg-primary-background/30 p-2')
|
||||
"
|
||||
:title="label ?? widgetName"
|
||||
:sub-title="subLabel"
|
||||
:rename
|
||||
:remove="
|
||||
() =>
|
||||
remove(
|
||||
appModeStore.selectedInputs,
|
||||
([id, name]) => nodeId == id && widgetName === name
|
||||
)
|
||||
"
|
||||
/>
|
||||
</DraggableList>
|
||||
</PropertiesAccordionItem>
|
||||
<PropertiesAccordionItem
|
||||
v-if="isSelectOutputsMode"
|
||||
:label="t('nodeHelpPage.outputs')"
|
||||
enable-empty-state
|
||||
:disabled="!appModeStore.selectedOutputs.length"
|
||||
:tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
|
||||
:tooltip-delay="100"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
{{ t('nodeHelpPage.outputs') }}
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('linearMode.builder.promptAddOutputs')"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('linearMode.builder.promptAddOutputs')"
|
||||
/>
|
||||
<DraggableList
|
||||
v-slot="{ dragClass }"
|
||||
v-model="appModeStore.selectedOutputs"
|
||||
>
|
||||
<IoItem
|
||||
v-for="([key, title], index) in outputsWithState"
|
||||
:key
|
||||
:class="
|
||||
cn(
|
||||
dragClass,
|
||||
'my-2 rounded-lg bg-warning-background/40 p-2',
|
||||
index === 0 && 'ring-2 ring-warning-background'
|
||||
)
|
||||
"
|
||||
:title
|
||||
:sub-title="String(key)"
|
||||
:remove="
|
||||
() => remove(appModeStore.selectedOutputs, (k) => k == key)
|
||||
"
|
||||
/>
|
||||
</DraggableList>
|
||||
</PropertiesAccordionItem>
|
||||
</div>
|
||||
<div class="flex items-center border-b border-border-subtle p-2 font-bold">
|
||||
{{
|
||||
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
|
||||
}}
|
||||
</div>
|
||||
<DraggableList
|
||||
v-if="isArrangeMode"
|
||||
v-slot="{ dragClass }"
|
||||
v-model="appModeStore.selectedInputs"
|
||||
>
|
||||
<div
|
||||
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
|
||||
:key="`${nodeId}: ${widgetName}`"
|
||||
:class="cn(dragClass, 'pointer-events-auto my-2 p-2')"
|
||||
:aria-label="`${widget?.label ?? widgetName} — ${node.title}`"
|
||||
>
|
||||
<div v-if="widget" class="pointer-events-none" inert>
|
||||
<WidgetItem
|
||||
:widget="widget"
|
||||
:node="node"
|
||||
show-node-name
|
||||
hidden-widget-actions
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="pointer-events-none p-1 text-sm text-muted-foreground">
|
||||
{{ widgetName }}
|
||||
<p class="text-xs italic">
|
||||
({{ t('linearMode.builder.unknownWidget') }})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DraggableList>
|
||||
<PropertiesAccordionItem
|
||||
v-if="isSelectInputsMode"
|
||||
:label="t('nodeHelpPage.inputs')"
|
||||
enable-empty-state
|
||||
:disabled="!appModeStore.selectedInputs.length"
|
||||
class="border-b border-border-subtle"
|
||||
:tooltip="`${t('linearMode.builder.inputsDesc')}\n${t('linearMode.builder.inputsExample')}`"
|
||||
:tooltip-delay="100"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
{{ t('nodeHelpPage.inputs') }}
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('linearMode.builder.promptAddInputs')"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('linearMode.builder.promptAddInputs')"
|
||||
/>
|
||||
<DraggableList v-slot="{ dragClass }" v-model="appModeStore.selectedInputs">
|
||||
<IoItem
|
||||
v-for="{
|
||||
nodeId,
|
||||
widgetName,
|
||||
label,
|
||||
subLabel,
|
||||
rename
|
||||
} in inputsWithState"
|
||||
:key="`${nodeId}: ${widgetName}`"
|
||||
:class="cn(dragClass, 'my-2 rounded-lg bg-primary-background/30 p-2')"
|
||||
:title="label ?? widgetName"
|
||||
:sub-title="subLabel"
|
||||
:rename
|
||||
:remove="
|
||||
() =>
|
||||
remove(
|
||||
appModeStore.selectedInputs,
|
||||
([id, name]) => nodeId == id && widgetName === name
|
||||
)
|
||||
"
|
||||
/>
|
||||
</DraggableList>
|
||||
</PropertiesAccordionItem>
|
||||
<PropertiesAccordionItem
|
||||
v-if="isSelectOutputsMode"
|
||||
:label="t('nodeHelpPage.outputs')"
|
||||
enable-empty-state
|
||||
:disabled="!appModeStore.selectedOutputs.length"
|
||||
:tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
|
||||
:tooltip-delay="100"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
{{ t('nodeHelpPage.outputs') }}
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('linearMode.builder.promptAddOutputs')"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('linearMode.builder.promptAddOutputs')"
|
||||
/>
|
||||
<DraggableList
|
||||
v-slot="{ dragClass }"
|
||||
v-model="appModeStore.selectedOutputs"
|
||||
>
|
||||
<IoItem
|
||||
v-for="([key, title], index) in outputsWithState"
|
||||
:key
|
||||
:class="
|
||||
cn(
|
||||
dragClass,
|
||||
'my-2 rounded-lg bg-warning-background/40 p-2',
|
||||
index === 0 && 'ring-2 ring-warning-background'
|
||||
)
|
||||
"
|
||||
:title
|
||||
:sub-title="String(key)"
|
||||
:remove="() => remove(appModeStore.selectedOutputs, (k) => k == key)"
|
||||
/>
|
||||
</DraggableList>
|
||||
</PropertiesAccordionItem>
|
||||
|
||||
<Teleport
|
||||
v-if="isSelectMode && !settingStore.get('Comfy.VueNodes.Enabled')"
|
||||
|
||||
@@ -19,31 +19,38 @@
|
||||
</button>
|
||||
</template>
|
||||
<template #default="{ close }">
|
||||
<template v-for="(item, index) in menuItems" :key="item.label">
|
||||
<div v-if="index > 0" class="my-1 border-t border-border-default" />
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
class="flex w-full items-center justify-start gap-3 rounded-md px-3 py-2 text-sm"
|
||||
:disabled="item.disabled"
|
||||
@click="item.action(close)"
|
||||
>
|
||||
<i :class="cn(item.icon, 'size-4')" />
|
||||
{{ item.label }}
|
||||
</Button>
|
||||
</template>
|
||||
<button
|
||||
:class="
|
||||
cn(
|
||||
'flex w-full items-center gap-3 rounded-md border-none bg-transparent px-3 py-2 text-sm',
|
||||
hasOutputs
|
||||
? 'cursor-pointer hover:bg-secondary-background-hover'
|
||||
: 'pointer-events-none opacity-50'
|
||||
)
|
||||
"
|
||||
:disabled="!hasOutputs"
|
||||
@click="onSave(close)"
|
||||
>
|
||||
<i class="icon-[lucide--save] size-4" />
|
||||
{{ t('g.save') }}
|
||||
</button>
|
||||
<div class="my-1 border-t border-border-default" />
|
||||
<button
|
||||
class="flex w-full cursor-pointer items-center gap-3 rounded-md border-none bg-transparent px-3 py-2 text-sm hover:bg-secondary-background-hover"
|
||||
@click="onExitBuilder(close)"
|
||||
>
|
||||
<i class="icon-[lucide--square-pen] size-4" />
|
||||
{{ t('builderMenu.exitAppBuilder') }}
|
||||
</button>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -53,30 +60,10 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
const { t } = useI18n()
|
||||
const appModeStore = useAppModeStore()
|
||||
const { hasOutputs } = storeToRefs(appModeStore)
|
||||
const { setMode } = useAppMode()
|
||||
const workflowService = useWorkflowService()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
label: t('g.save'),
|
||||
icon: 'icon-[lucide--save]',
|
||||
disabled: !hasOutputs.value,
|
||||
action: onSave
|
||||
},
|
||||
{
|
||||
label: t('builderMenu.enterAppMode'),
|
||||
icon: 'icon-[lucide--panels-top-left]',
|
||||
action: onEnterAppMode
|
||||
},
|
||||
{
|
||||
label: t('builderMenu.exitAppBuilder'),
|
||||
icon: 'icon-[lucide--square-pen]',
|
||||
action: onExitBuilder
|
||||
}
|
||||
])
|
||||
|
||||
async function onSave(close: () => void) {
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
if (!workflow) return
|
||||
@@ -88,11 +75,6 @@ async function onSave(close: () => void) {
|
||||
}
|
||||
}
|
||||
|
||||
function onEnterAppMode(close: () => void) {
|
||||
setMode('app')
|
||||
close()
|
||||
}
|
||||
|
||||
function onExitBuilder(close: () => void) {
|
||||
void appModeStore.exitBuilder()
|
||||
close()
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import Loader from './Loader.vue'
|
||||
|
||||
const meta: Meta<typeof Loader> = {
|
||||
title: 'Components/Loader/Loader',
|
||||
title: 'Components/Common/Loader',
|
||||
component: Loader,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
DropdownMenuRoot,
|
||||
DropdownMenuTrigger
|
||||
} from 'reka-ui'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import WorkflowActionsList from '@/components/common/WorkflowActionsList.vue'
|
||||
@@ -23,7 +22,6 @@ const { source, align = 'start' } = defineProps<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const dropdownOpen = ref(false)
|
||||
|
||||
const { menuItems } = useWorkflowActionsMenu(
|
||||
() => useCommandStore().execute('Comfy.RenameWorkflow'),
|
||||
@@ -42,38 +40,22 @@ function handleOpen(open: boolean) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLinearMode() {
|
||||
dropdownOpen.value = false
|
||||
void useCommandStore().execute('Comfy.ToggleLinear', {
|
||||
metadata: { source }
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot v-model:open="dropdownOpen" @update:open="handleOpen">
|
||||
<slot name="button" :has-unseen-items="hasUnseenItems">
|
||||
<div
|
||||
class="pointer-events-auto inline-flex items-center rounded-lg bg-secondary-background"
|
||||
>
|
||||
<DropdownMenuRoot @update:open="handleOpen">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<slot name="button" :has-unseen-items="hasUnseenItems">
|
||||
<Button
|
||||
v-tooltip="{
|
||||
value: canvasStore.linearMode
|
||||
? t('breadcrumbsMenu.enterNodeGraph')
|
||||
: t('breadcrumbsMenu.enterAppMode'),
|
||||
value: t('breadcrumbsMenu.workflowActions'),
|
||||
showDelay: 300,
|
||||
hideDelay: 300
|
||||
}"
|
||||
:aria-label="
|
||||
canvasStore.linearMode
|
||||
? t('breadcrumbsMenu.enterNodeGraph')
|
||||
: t('breadcrumbsMenu.enterAppMode')
|
||||
"
|
||||
variant="base"
|
||||
class="m-1"
|
||||
@pointerdown.stop
|
||||
@click="toggleLinearMode"
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:aria-label="t('breadcrumbsMenu.workflowActions')"
|
||||
class="pointer-events-auto relative h-10 gap-1 rounded-lg pr-2 pl-3 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
|
||||
>
|
||||
<i
|
||||
class="size-4"
|
||||
@@ -83,36 +65,15 @@ function toggleLinearMode() {
|
||||
: 'icon-[comfy--workflow]'
|
||||
"
|
||||
/>
|
||||
<i class="icon-[lucide--chevron-down] size-4 text-muted-foreground" />
|
||||
<span
|
||||
v-if="hasUnseenItems"
|
||||
aria-hidden="true"
|
||||
class="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-primary-background"
|
||||
/>
|
||||
</Button>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button
|
||||
v-tooltip="{
|
||||
value: t('breadcrumbsMenu.workflowActions'),
|
||||
showDelay: 300,
|
||||
hideDelay: 300
|
||||
}"
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:aria-label="t('breadcrumbsMenu.workflowActions')"
|
||||
class="relative h-10 gap-1 rounded-lg pr-2 pl-2.5 text-center data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
|
||||
>
|
||||
<span>{{
|
||||
canvasStore.linearMode
|
||||
? t('breadcrumbsMenu.app')
|
||||
: t('breadcrumbsMenu.graph')
|
||||
}}</span>
|
||||
<i
|
||||
class="icon-[lucide--chevron-down] size-4 text-muted-foreground"
|
||||
/>
|
||||
<span
|
||||
v-if="hasUnseenItems"
|
||||
aria-hidden="true"
|
||||
class="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-primary-background"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
</slot>
|
||||
</slot>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
:align
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
|
||||
import {
|
||||
fetchModelMetadata,
|
||||
getBadgeLabel,
|
||||
hasValidDirectory,
|
||||
isModelDownloadable
|
||||
} from '@/components/dialog/content/missingModelsUtils'
|
||||
import type { ModelWithUrl } from '@/components/dialog/content/missingModelsUtils'
|
||||
import { fetchModelMetadata } from './missingModelsUtils'
|
||||
|
||||
const fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
@@ -14,84 +8,6 @@ vi.stubGlobal('fetch', fetchMock)
|
||||
vi.mock('@/platform/distribution/types', () => ({ isDesktop: false }))
|
||||
vi.mock('@/stores/electronDownloadStore', () => ({}))
|
||||
|
||||
function makeModel(overrides: Partial<ModelWithUrl> = {}): ModelWithUrl {
|
||||
return {
|
||||
name: 'model.safetensors',
|
||||
url: 'https://civitai.com/api/download/12345',
|
||||
directory: 'checkpoints',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('isModelDownloadable', () => {
|
||||
it('allows civitai URLs with valid suffix', () => {
|
||||
expect(isModelDownloadable(makeModel())).toBe(true)
|
||||
})
|
||||
|
||||
it('allows huggingface URLs with valid suffix', () => {
|
||||
expect(
|
||||
isModelDownloadable(
|
||||
makeModel({
|
||||
url: 'https://huggingface.co/some/model',
|
||||
name: 'model.ckpt'
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('allows localhost URLs with valid suffix', () => {
|
||||
expect(
|
||||
isModelDownloadable(makeModel({ url: 'http://localhost:8080/model' }))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects URLs from unknown sources', () => {
|
||||
expect(
|
||||
isModelDownloadable(
|
||||
makeModel({ url: 'https://evil.com/model.safetensors' })
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects files with invalid suffix', () => {
|
||||
expect(isModelDownloadable(makeModel({ name: 'model.exe' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('allows whitelisted URLs regardless of suffix', () => {
|
||||
expect(
|
||||
isModelDownloadable(
|
||||
makeModel({
|
||||
url: 'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth',
|
||||
name: 'RealESRGAN_x4plus.pth'
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasValidDirectory', () => {
|
||||
it('returns true when directory exists in paths', () => {
|
||||
const paths = { checkpoints: ['/models/checkpoints'] }
|
||||
expect(hasValidDirectory(makeModel(), paths)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when directory is missing', () => {
|
||||
expect(hasValidDirectory(makeModel(), {})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBadgeLabel', () => {
|
||||
it('maps known directories to badge labels', () => {
|
||||
expect(getBadgeLabel('vae')).toBe('VAE')
|
||||
expect(getBadgeLabel('loras')).toBe('LORA')
|
||||
expect(getBadgeLabel('checkpoints')).toBe('CHECKPOINT')
|
||||
})
|
||||
|
||||
it('uppercases unknown directories', () => {
|
||||
expect(getBadgeLabel('custom_dir')).toBe('CUSTOM_DIR')
|
||||
})
|
||||
})
|
||||
|
||||
let testId = 0
|
||||
|
||||
describe('fetchModelMetadata', () => {
|
||||
|
||||
@@ -149,7 +149,7 @@ import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||
import { useCanvasDrop } from '@/composables/useCanvasDrop'
|
||||
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
|
||||
import { useGraphCopyHandler } from '@/composables/useGraphCopyHandler'
|
||||
import { useCopy } from '@/composables/useCopy'
|
||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||
import { usePaste } from '@/composables/usePaste'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
@@ -446,7 +446,7 @@ useNodeBadge()
|
||||
|
||||
useGlobalLitegraph()
|
||||
useContextMenuTranslation()
|
||||
useGraphCopyHandler()
|
||||
useCopy()
|
||||
usePaste()
|
||||
useWorkflowAutoSave()
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<ZoomControlsModal :visible="isPopoverOpen" @close="hidePopover" />
|
||||
<ZoomControlsModal :visible="isModalVisible" @close="hideModal" />
|
||||
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
v-if="hasActivePopup"
|
||||
class="fixed inset-0 z-1200"
|
||||
@click="hidePopover"
|
||||
@click="hideModal"
|
||||
></div>
|
||||
|
||||
<ButtonGroup
|
||||
@@ -40,7 +40,7 @@
|
||||
:aria-label="t('zoomControls.label')"
|
||||
data-testid="zoom-controls-button"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
@click="togglePopover"
|
||||
@click="toggleModal"
|
||||
>
|
||||
<span class="inline-flex items-center gap-1 px-2 text-xs">
|
||||
<span>{{ canvasStore.appScalePercentage }}%</span>
|
||||
@@ -110,7 +110,7 @@ const settingStore = useSettingStore()
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
const minimap = useMinimap()
|
||||
|
||||
const { isPopoverOpen, togglePopover, hidePopover, hasActivePopup } =
|
||||
const { isModalVisible, toggleModal, hideModal, hasActivePopup } =
|
||||
useZoomControls()
|
||||
|
||||
const stringifiedMinimapStyles = computed(() => {
|
||||
@@ -157,7 +157,7 @@ const minimapCommandText = computed(() =>
|
||||
// Computed properties for button classes and states
|
||||
const zoomButtonClass = computed(() => [
|
||||
'bg-comfy-menu-bg',
|
||||
isPopoverOpen.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
|
||||
isModalVisible.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
|
||||
'hover:bg-interface-button-hover-surface!',
|
||||
'p-0',
|
||||
'h-8',
|
||||
|
||||
@@ -76,7 +76,7 @@ import Load3DScene from '@/components/load3d/Load3DScene.vue'
|
||||
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
|
||||
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
|
||||
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
|
||||
import { useLoad3d } from '@/extensions/core/load3d/composables/useLoad3d'
|
||||
import { useLoad3d } from '@/composables/useLoad3d'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
|
||||
import { useLoad3dDrag } from '@/extensions/core/load3d/composables/useLoad3dDrag'
|
||||
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
|
||||
|
||||
const props = defineProps<{
|
||||
initializeLoad3d: (containerRef: HTMLElement) => Promise<void>
|
||||
|
||||
@@ -103,8 +103,8 @@ import LightControls from '@/components/load3d/controls/viewer/ViewerLightContro
|
||||
import ModelControls from '@/components/load3d/controls/viewer/ViewerModelControls.vue'
|
||||
import SceneControls from '@/components/load3d/controls/viewer/ViewerSceneControls.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useLoad3dDrag } from '@/extensions/core/load3d/composables/useLoad3dDrag'
|
||||
import { useLoad3dViewer } from '@/extensions/core/load3d/composables/useLoad3dViewer'
|
||||
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
|
||||
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import LogoCFillLoader from './LogoCFillLoader.vue'
|
||||
|
||||
const meta: Meta<typeof LogoCFillLoader> = {
|
||||
title: 'Components/Loader/LogoCFillLoader',
|
||||
component: LogoCFillLoader,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
backgrounds: { default: 'dark' }
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['sm', 'md', 'lg', 'xl']
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['yellow', 'blue', 'white', 'black']
|
||||
},
|
||||
bordered: {
|
||||
control: 'boolean'
|
||||
},
|
||||
disableAnimation: {
|
||||
control: 'boolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Small: Story = {
|
||||
args: { size: 'sm' }
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: { size: 'lg' }
|
||||
}
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: { size: 'xl' }
|
||||
}
|
||||
|
||||
export const NoBorder: Story = {
|
||||
args: { bordered: false }
|
||||
}
|
||||
|
||||
export const Static: Story = {
|
||||
args: { disableAnimation: true }
|
||||
}
|
||||
|
||||
export const BrandColors: Story = {
|
||||
render: () => ({
|
||||
components: { LogoCFillLoader },
|
||||
template: `
|
||||
<div class="flex items-end gap-12">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">Yellow</span>
|
||||
<LogoCFillLoader size="lg" color="yellow" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">Blue</span>
|
||||
<LogoCFillLoader size="lg" color="blue" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">White</span>
|
||||
<LogoCFillLoader size="lg" color="white" />
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded" style="background: white">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-600">Black</span>
|
||||
<LogoCFillLoader size="lg" color="black" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: () => ({
|
||||
components: { LogoCFillLoader },
|
||||
template: `
|
||||
<div class="flex items-end gap-8">
|
||||
<LogoCFillLoader size="sm" color="yellow" />
|
||||
<LogoCFillLoader size="md" color="yellow" />
|
||||
<LogoCFillLoader size="lg" color="yellow" />
|
||||
<LogoCFillLoader size="xl" color="yellow" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<span role="status" :class="cn('inline-flex', colorClass)">
|
||||
<svg
|
||||
:width="Math.round(heightMap[size] * (VB_W / VB_H))"
|
||||
:height="heightMap[size]"
|
||||
:viewBox="`0 0 ${VB_W} ${VB_H}`"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<mask :id="maskId">
|
||||
<path :d="C_PATH" fill="white" />
|
||||
</mask>
|
||||
</defs>
|
||||
<path
|
||||
v-if="bordered"
|
||||
:d="C_PATH"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
opacity="0.4"
|
||||
/>
|
||||
<g :mask="`url(#${maskId})`">
|
||||
<rect
|
||||
:class="disableAnimation ? undefined : 'c-fill-rect'"
|
||||
:x="-BLEED"
|
||||
:y="-BLEED"
|
||||
:width="VB_W + BLEED * 2"
|
||||
:height="VB_H + BLEED * 2"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<span class="sr-only">{{ t('g.loading') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useId, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
color = 'black',
|
||||
bordered = true,
|
||||
disableAnimation = false
|
||||
} = defineProps<{
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
color?: 'yellow' | 'blue' | 'white' | 'black'
|
||||
bordered?: boolean
|
||||
disableAnimation?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const maskId = `c-mask-${useId()}`
|
||||
|
||||
const VB_W = 185
|
||||
const VB_H = 201
|
||||
const BLEED = 1
|
||||
|
||||
// Larger than LogoComfyWaveLoader because the C logo is near-square (185×201)
|
||||
// while the COMFY wordmark is wide (879×284), so larger heights are needed
|
||||
// for visually comparable perceived size.
|
||||
const heightMap = { sm: 48, md: 80, lg: 120, xl: 200 } as const
|
||||
const colorMap = {
|
||||
yellow: 'text-brand-yellow',
|
||||
blue: 'text-brand-blue',
|
||||
white: 'text-white',
|
||||
black: 'text-black'
|
||||
} as const
|
||||
|
||||
const colorClass = computed(() => colorMap[color])
|
||||
|
||||
const C_PATH =
|
||||
'M42.1217 200.812C37.367 200.812 33.5304 199.045 31.0285 195.703C28.4569 192.27 27.7864 187.477 29.1882 182.557L34.8172 162.791C35.2661 161.217 34.9537 159.523 33.9747 158.214C32.9958 156.908 31.464 156.139 29.8371 156.139L13.6525 156.139C8.89521 156.139 5.05862 154.374 2.55797 151.032C-0.0136533 147.597-0.684085 142.804 0.71869 137.883L20.0565 70.289L22.1916 62.8625C25.0617 52.7847 35.5288 44.5943 45.528 44.5943L64.8938 44.5943C67.2048 44.5943 69.2376 43.0535 69.8738 40.8175L76.2782 18.3344C79.1454 8.26681 89.6127 0.0763962 99.6117 0.0763945L141.029 0.00258328L171.349-2.99253e-05C176.104-3.0756e-05 179.941 1.765 182.442 5.10626C185.013 8.53932 185.684 13.3324 184.282 18.2528L175.612 48.6947C172.746 58.7597 162.279 66.9475 152.28 66.9475L110.771 67.0265L91.4113 67.0265C89.1029 67.0265 87.0727 68.5647 86.4326 70.7983L70.2909 127.179C69.8394 128.756 70.1518 130.454 71.1334 131.763C72.1123 133.07 73.6441 133.839 75.2697 133.839C75.2736 133.839 102.699 133.785 102.699 133.785L132.929 133.785C137.685 133.785 141.522 135.55 144.023 138.892C146.594 142.327 147.265 147.12 145.862 152.041L137.192 182.478C134.326 192.545 123.859 200.733 113.86 200.733L72.3517 200.812L42.1217 200.812Z'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.c-fill-rect {
|
||||
animation: c-fill-up 2.5s cubic-bezier(0.25, 0, 0.3, 1) forwards;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes c-fill-up {
|
||||
0% {
|
||||
transform: translateY(calc(v-bind(VB_H) * 1px + v-bind(BLEED) * 1px));
|
||||
}
|
||||
100% {
|
||||
transform: translateY(calc(v-bind(BLEED) * -1px));
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.c-fill-rect {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,96 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import LogoComfyWaveLoader from './LogoComfyWaveLoader.vue'
|
||||
|
||||
const meta: Meta<typeof LogoComfyWaveLoader> = {
|
||||
title: 'Components/Loader/LogoComfyWaveLoader',
|
||||
component: LogoComfyWaveLoader,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
backgrounds: { default: 'dark' }
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['sm', 'md', 'lg', 'xl']
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['yellow', 'blue', 'white', 'black']
|
||||
},
|
||||
bordered: {
|
||||
control: 'boolean'
|
||||
},
|
||||
disableAnimation: {
|
||||
control: 'boolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Small: Story = {
|
||||
args: { size: 'sm' }
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: { size: 'lg' }
|
||||
}
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: { size: 'xl' }
|
||||
}
|
||||
|
||||
export const NoBorder: Story = {
|
||||
args: { bordered: false }
|
||||
}
|
||||
|
||||
export const Static: Story = {
|
||||
args: { disableAnimation: true }
|
||||
}
|
||||
|
||||
export const BrandColors: Story = {
|
||||
render: () => ({
|
||||
components: { LogoComfyWaveLoader },
|
||||
template: `
|
||||
<div class="flex flex-col items-center gap-12">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">#F0FF41 (Yellow)</span>
|
||||
<LogoComfyWaveLoader size="lg" color="yellow" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">#172DD7 (Blue)</span>
|
||||
<LogoComfyWaveLoader size="lg" color="blue" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-400">White</span>
|
||||
<LogoComfyWaveLoader size="lg" color="white" />
|
||||
</div>
|
||||
<div class="p-4 bg-white rounded" style="background: white">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-xs text-neutral-600">Black</span>
|
||||
<LogoComfyWaveLoader size="lg" color="black" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: () => ({
|
||||
components: { LogoComfyWaveLoader },
|
||||
template: `
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<LogoComfyWaveLoader size="sm" color="yellow" />
|
||||
<LogoComfyWaveLoader size="md" color="yellow" />
|
||||
<LogoComfyWaveLoader size="lg" color="yellow" />
|
||||
<LogoComfyWaveLoader size="xl" color="yellow" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
@@ -200,7 +200,7 @@ const onMaskOpacityChange = (value: number) => {
|
||||
maskLayerVisible.value = value !== 0
|
||||
}
|
||||
|
||||
const onBlendModeChange = (event: Event) => {
|
||||
const onBlendModeChange = async (event: Event) => {
|
||||
const value = (event.target as HTMLSelectElement).value
|
||||
let blendMode: MaskBlendMode
|
||||
|
||||
@@ -217,7 +217,7 @@ const onBlendModeChange = (event: Event) => {
|
||||
|
||||
store.maskBlendMode = blendMode
|
||||
|
||||
canvasManager.updateMaskColor()
|
||||
await canvasManager.updateMaskColor()
|
||||
}
|
||||
|
||||
const setActiveLayer = (layer: ImageLayer) => {
|
||||
|
||||
@@ -149,7 +149,7 @@ const initUI = async () => {
|
||||
const imageLoader = useImageLoader()
|
||||
const image = await imageLoader.loadImages()
|
||||
|
||||
panZoom.initializeCanvasPanZoom(
|
||||
await panZoom.initializeCanvasPanZoom(
|
||||
image,
|
||||
containerRef.value,
|
||||
toolPanelRef.value?.$el as HTMLElement | undefined,
|
||||
@@ -181,9 +181,9 @@ onMounted(() => {
|
||||
keyboard.addListeners()
|
||||
|
||||
if (containerRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
resizeObserver = new ResizeObserver(async () => {
|
||||
if (panZoom) {
|
||||
panZoom.invalidatePanZoom()
|
||||
await panZoom.invalidatePanZoom()
|
||||
}
|
||||
})
|
||||
resizeObserver.observe(containerRef.value)
|
||||
|
||||
@@ -79,16 +79,16 @@ const handleTouchStart = (event: TouchEvent) => {
|
||||
panZoom.handleTouchStart(event)
|
||||
}
|
||||
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
panZoom.handleTouchMove(event)
|
||||
const handleTouchMove = async (event: TouchEvent) => {
|
||||
await panZoom.handleTouchMove(event)
|
||||
}
|
||||
|
||||
const handleTouchEnd = (event: TouchEvent) => {
|
||||
panZoom.handleTouchEnd(event)
|
||||
}
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
panZoom.zoom(event)
|
||||
const handleWheel = async (event: WheelEvent) => {
|
||||
await panZoom.zoom(event)
|
||||
const newCursorPoint = { x: event.clientX, y: event.clientY }
|
||||
panZoom.updateCursorPosition(newCursorPoint)
|
||||
}
|
||||
|
||||
@@ -161,33 +161,33 @@ const onRedo = () => {
|
||||
store.canvasHistory.redo()
|
||||
}
|
||||
|
||||
const onRotateLeft = () => {
|
||||
const onRotateLeft = async () => {
|
||||
try {
|
||||
canvasTransform.rotateCounterclockwise()
|
||||
await canvasTransform.rotateCounterclockwise()
|
||||
} catch (error) {
|
||||
console.error('[TopBarHeader] Rotate left failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onRotateRight = () => {
|
||||
const onRotateRight = async () => {
|
||||
try {
|
||||
canvasTransform.rotateClockwise()
|
||||
await canvasTransform.rotateClockwise()
|
||||
} catch (error) {
|
||||
console.error('[TopBarHeader] Rotate right failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onMirrorHorizontal = () => {
|
||||
const onMirrorHorizontal = async () => {
|
||||
try {
|
||||
canvasTransform.mirrorHorizontal()
|
||||
await canvasTransform.mirrorHorizontal()
|
||||
} catch (error) {
|
||||
console.error('[TopBarHeader] Mirror horizontal failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onMirrorVertical = () => {
|
||||
const onMirrorVertical = async () => {
|
||||
try {
|
||||
canvasTransform.mirrorVertical()
|
||||
await canvasTransform.mirrorVertical()
|
||||
} catch (error) {
|
||||
console.error('[TopBarHeader] Mirror vertical failed:', error)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import QueueOverlayActive from './QueueOverlayActive.vue'
|
||||
import * as tooltipConfig from '@/utils/tooltipConfig'
|
||||
import * as tooltipConfig from '@/composables/useTooltipConfig'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
|
||||
@@ -95,7 +95,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
defineProps<{
|
||||
totalProgressStyle: Record<string, string>
|
||||
|
||||
@@ -48,7 +48,7 @@ vi.mock('@/stores/workspace/sidebarTabStore', () => ({
|
||||
}))
|
||||
|
||||
import QueueOverlayHeader from './QueueOverlayHeader.vue'
|
||||
import * as tooltipConfig from '@/utils/tooltipConfig'
|
||||
import * as tooltipConfig from '@/composables/useTooltipConfig'
|
||||
|
||||
const tooltipDirectiveStub = {
|
||||
mounted: vi.fn(),
|
||||
|
||||
@@ -32,7 +32,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import JobHistoryActionsMenu from '@/components/queue/JobHistoryActionsMenu.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
defineProps<{
|
||||
headerTitle: string
|
||||
|
||||
@@ -121,7 +121,7 @@ import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { jobSortModes } from '@/composables/queue/useJobList'
|
||||
import type { JobSortMode } from '@/composables/queue/useJobList'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
const {
|
||||
hideShowAssetsAction = false,
|
||||
|
||||
@@ -193,15 +193,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue'
|
||||
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import {
|
||||
progressBarContainerClass,
|
||||
progressBarPrimaryClass,
|
||||
progressBarSecondaryClass,
|
||||
hasProgressPercent,
|
||||
hasAnyProgressPercent,
|
||||
progressPercentStyle
|
||||
} from '@/composables/useProgressBarBackground'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import type { JobState } from '@/types/queue'
|
||||
import { iconForJobState } from '@/utils/queueDisplay'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -244,6 +237,14 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const {
|
||||
progressBarContainerClass,
|
||||
progressBarPrimaryClass,
|
||||
progressBarSecondaryClass,
|
||||
hasProgressPercent,
|
||||
hasAnyProgressPercent,
|
||||
progressPercentStyle
|
||||
} = useProgressBarBackground()
|
||||
|
||||
const cancelTooltipConfig = computed(() => buildTooltipConfig(t('g.cancel')))
|
||||
const deleteTooltipConfig = computed(() => buildTooltipConfig(t('g.delete')))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<BaseWorkflowsSidebarTab
|
||||
:title="$t('linearMode.appModeToolbar.apps')"
|
||||
:filter="isAppWorkflow"
|
||||
:label-transform="stripAppJsonSuffix"
|
||||
hide-leaf-icon
|
||||
:search-subject="$t('linearMode.appModeToolbar.apps')"
|
||||
data-testid="apps-sidebar"
|
||||
@@ -17,14 +18,8 @@
|
||||
<NoResultsPlaceholder
|
||||
button-variant="secondary"
|
||||
text-class="text-muted-foreground text-sm"
|
||||
:message="
|
||||
isAppMode
|
||||
? $t('linearMode.appModeToolbar.appsEmptyMessage')
|
||||
: `${$t('linearMode.appModeToolbar.appsEmptyMessage')}\n${$t('linearMode.appModeToolbar.appsEmptyMessageAction')}`
|
||||
"
|
||||
:button-label="
|
||||
isAppMode ? undefined : $t('linearMode.appModeToolbar.enterAppMode')
|
||||
"
|
||||
:message="$t('linearMode.appModeToolbar.appsEmptyMessage')"
|
||||
:button-label="$t('linearMode.appModeToolbar.enterAppMode')"
|
||||
@action="enterAppMode"
|
||||
/>
|
||||
</template>
|
||||
@@ -37,12 +32,16 @@ import BaseWorkflowsSidebarTab from '@/components/sidebar/tabs/BaseWorkflowsSide
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
|
||||
const { isAppMode, setMode } = useAppMode()
|
||||
const { setMode } = useAppMode()
|
||||
|
||||
function isAppWorkflow(workflow: ComfyWorkflow): boolean {
|
||||
return workflow.suffix === 'app.json'
|
||||
}
|
||||
|
||||
function stripAppJsonSuffix(label: string): string {
|
||||
return label.replace(/\.app\.json$/i, '')
|
||||
}
|
||||
|
||||
function enterAppMode() {
|
||||
setMode('app')
|
||||
}
|
||||
|
||||
@@ -533,8 +533,8 @@ const handleBulkDelete = async (assets: AssetItem[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleBulkAddToWorkflow = (assets: AssetItem[]) => {
|
||||
addMultipleToWorkflow(assets)
|
||||
const handleBulkAddToWorkflow = async (assets: AssetItem[]) => {
|
||||
await addMultipleToWorkflow(assets)
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,6 @@ import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue
|
||||
import WorkflowTreeLeaf from '@/components/sidebar/tabs/workflows/WorkflowTreeLeaf.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useTreeExpansion } from '@/composables/useTreeExpansion'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import {
|
||||
@@ -164,23 +163,26 @@ import {
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { TreeExplorerNode, TreeNode } from '@/types/treeExplorerTypes'
|
||||
import {
|
||||
ensureWorkflowSuffix,
|
||||
getFilenameDetails,
|
||||
getWorkflowSuffix
|
||||
} from '@/utils/formatUtil'
|
||||
import { ensureWorkflowSuffix, getWorkflowSuffix } from '@/utils/formatUtil'
|
||||
import { buildTree, sortedTree } from '@/utils/treeUtil'
|
||||
|
||||
const { title, filter, searchSubject, dataTestid, hideLeafIcon } = defineProps<{
|
||||
const {
|
||||
title,
|
||||
filter,
|
||||
searchSubject,
|
||||
dataTestid,
|
||||
labelTransform,
|
||||
hideLeafIcon
|
||||
} = defineProps<{
|
||||
title: string
|
||||
filter?: (workflow: ComfyWorkflow) => boolean
|
||||
searchSubject: string
|
||||
dataTestid: string
|
||||
labelTransform?: (label: string) => string
|
||||
hideLeafIcon?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { isAppMode } = useAppMode()
|
||||
|
||||
const applyFilter = (workflows: ComfyWorkflow[]) =>
|
||||
filter ? workflows.filter(filter) : workflows
|
||||
@@ -302,18 +304,14 @@ const renderTreeNode = (
|
||||
},
|
||||
contextMenuItems() {
|
||||
return [
|
||||
...(isAppMode.value
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: t('g.insert'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: async () => {
|
||||
const workflow = node.data
|
||||
await workflowService.insertWorkflow(workflow)
|
||||
}
|
||||
}
|
||||
]),
|
||||
{
|
||||
label: t('g.insert'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: async () => {
|
||||
const workflow = node.data
|
||||
await workflowService.insertWorkflow(workflow)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('g.duplicate'),
|
||||
icon: 'pi pi-file-export',
|
||||
@@ -328,7 +326,8 @@ const renderTreeNode = (
|
||||
}
|
||||
: { handleClick }
|
||||
|
||||
const label = node.leaf ? getFilenameDetails(node.label).filename : node.label
|
||||
const label =
|
||||
node.leaf && labelTransform ? labelTransform(node.label) : node.label
|
||||
|
||||
return {
|
||||
key: node.key,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
import Loader from '@/components/common/Loader.vue'
|
||||
import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import type { AssetDownload } from '@/stores/assetDownloadStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@ const panY = ref(0.0)
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
const zoomPaneEl = zoomPane.value
|
||||
if (!zoomPaneEl || (e.deltaY < 0 ? zoom.value > 1200 : zoom.value < -500))
|
||||
return
|
||||
if (!zoomPaneEl) return
|
||||
|
||||
zoom.value -= e.deltaY
|
||||
const { x, y, width, height } = zoomPaneEl.getBoundingClientRect()
|
||||
|
||||
@@ -20,7 +20,6 @@ export const buttonVariants = cva({
|
||||
'destructive-textonly':
|
||||
'bg-transparent text-destructive-background hover:bg-destructive-background/10',
|
||||
'overlay-white': 'bg-white text-gray-600 hover:bg-white/90',
|
||||
base: 'bg-base-background text-base-foreground hover:bg-secondary-background-hover',
|
||||
gradient:
|
||||
'border-transparent bg-(image:--subscription-button-gradient) text-white hover:opacity-90'
|
||||
},
|
||||
@@ -50,7 +49,6 @@ const variants = [
|
||||
'textonly',
|
||||
'muted-textonly',
|
||||
'destructive-textonly',
|
||||
'base',
|
||||
'overlay-white',
|
||||
'gradient'
|
||||
] as const satisfies Array<ButtonVariants['variant']>
|
||||
|
||||
@@ -190,35 +190,39 @@ function useBillingContextInternal(): BillingContext {
|
||||
}
|
||||
}
|
||||
|
||||
function fetchStatus(): Promise<void> {
|
||||
async function fetchStatus(): Promise<void> {
|
||||
return activeContext.value.fetchStatus()
|
||||
}
|
||||
|
||||
function fetchBalance(): Promise<void> {
|
||||
async function fetchBalance(): Promise<void> {
|
||||
return activeContext.value.fetchBalance()
|
||||
}
|
||||
|
||||
function subscribe(planSlug: string, returnUrl?: string, cancelUrl?: string) {
|
||||
async function subscribe(
|
||||
planSlug: string,
|
||||
returnUrl?: string,
|
||||
cancelUrl?: string
|
||||
) {
|
||||
return activeContext.value.subscribe(planSlug, returnUrl, cancelUrl)
|
||||
}
|
||||
|
||||
function previewSubscribe(planSlug: string) {
|
||||
async function previewSubscribe(planSlug: string) {
|
||||
return activeContext.value.previewSubscribe(planSlug)
|
||||
}
|
||||
|
||||
function manageSubscription() {
|
||||
async function manageSubscription() {
|
||||
return activeContext.value.manageSubscription()
|
||||
}
|
||||
|
||||
function cancelSubscription() {
|
||||
async function cancelSubscription() {
|
||||
return activeContext.value.cancelSubscription()
|
||||
}
|
||||
|
||||
function fetchPlans() {
|
||||
async function fetchPlans() {
|
||||
return activeContext.value.fetchPlans()
|
||||
}
|
||||
|
||||
function requireActiveSubscription() {
|
||||
async function requireActiveSubscription() {
|
||||
return activeContext.value.requireActiveSubscription()
|
||||
}
|
||||
|
||||
|
||||
@@ -137,11 +137,11 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
await legacySubscribe()
|
||||
}
|
||||
|
||||
function previewSubscribe(
|
||||
async function previewSubscribe(
|
||||
_planSlug: string
|
||||
): Promise<PreviewSubscribeResponse | null> {
|
||||
// Legacy billing doesn't support preview - returns null
|
||||
return Promise.resolve(null)
|
||||
return null
|
||||
}
|
||||
|
||||
async function manageSubscription(): Promise<void> {
|
||||
@@ -152,10 +152,9 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
await legacyManageSubscription()
|
||||
}
|
||||
|
||||
function fetchPlans(): Promise<void> {
|
||||
async function fetchPlans(): Promise<void> {
|
||||
// Legacy billing doesn't have workspace-style plans
|
||||
// Plans are hardcoded in the UI for legacy subscriptions
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
async function requireActiveSubscription(): Promise<void> {
|
||||
|
||||
@@ -4,13 +4,28 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { collectFromNodes } from '@/utils/graphTraversalUtil'
|
||||
|
||||
/**
|
||||
* Composable for handling selected LiteGraph items filtering and operations.
|
||||
* This provides utilities for working with selected items on the canvas,
|
||||
* including filtering out items that should not be included in selection operations.
|
||||
*/
|
||||
export function useSelectedLiteGraphItems() {
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
/**
|
||||
* Items that should not show in the selection overlay are ignored.
|
||||
* @param item - The item to check.
|
||||
* @returns True if the item should be ignored, false otherwise.
|
||||
*/
|
||||
const isIgnoredItem = (item: Positionable): boolean => {
|
||||
return item instanceof Reroute
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out items that should not show in the selection overlay.
|
||||
* @param items - The Set of items to filter.
|
||||
* @returns The filtered Set of items.
|
||||
*/
|
||||
const filterSelectableItems = (
|
||||
items: Set<Positionable>
|
||||
): Set<Positionable> => {
|
||||
@@ -23,20 +38,37 @@ export function useSelectedLiteGraphItems() {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filtered selected items from the canvas.
|
||||
* @returns The filtered Set of selected items.
|
||||
*/
|
||||
const getSelectableItems = (): Set<Positionable> => {
|
||||
const { selectedItems } = canvasStore.getCanvas()
|
||||
return filterSelectableItems(selectedItems)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any selectable items.
|
||||
* @returns True if there are selectable items, false otherwise.
|
||||
*/
|
||||
const hasSelectableItems = (): boolean => {
|
||||
return getSelectableItems().size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are multiple selectable items.
|
||||
* @returns True if there are multiple selectable items, false otherwise.
|
||||
*/
|
||||
const hasMultipleSelectableItems = (): boolean => {
|
||||
return getSelectableItems().size > 1
|
||||
}
|
||||
|
||||
/** Includes descendant nodes from any selected subgraphs. */
|
||||
/**
|
||||
* Get only the selected nodes (LGraphNode instances) from the canvas.
|
||||
* This filters out other types of selected items like groups or reroutes.
|
||||
* If a selected node is a subgraph, this also includes all nodes within it.
|
||||
* @returns Array of selected LGraphNode instances and their descendants.
|
||||
*/
|
||||
const getSelectedNodes = (): LGraphNode[] => {
|
||||
const selectedNodes = app.canvas.selected_nodes
|
||||
if (!selectedNodes) return []
|
||||
|
||||
@@ -2,13 +2,13 @@ import type { CSSProperties } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import type { Point, Size } from '@/lib/litegraph/src/interfaces'
|
||||
import type { Size, Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
|
||||
export interface PositionConfig {
|
||||
/* The position of the element on litegraph canvas */
|
||||
pos: Point
|
||||
pos: Vector2
|
||||
/* The size of the element on litegraph canvas */
|
||||
size: Size
|
||||
/* The scale factor of the canvas */
|
||||
|
||||
@@ -530,6 +530,7 @@ function captureDynamicSubmenu(
|
||||
return converted
|
||||
}
|
||||
|
||||
console.warn('[ContextMenuConverter] No items captured for:', item.content)
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function useSubgraphOperations() {
|
||||
workflowStore.activeWorkflow?.changeTracker?.checkState()
|
||||
}
|
||||
|
||||
const unpackNodes = (
|
||||
const doUnpack = (
|
||||
subgraphNodes: SubgraphNode[],
|
||||
skipMissingNodes: boolean
|
||||
) => {
|
||||
@@ -65,7 +65,7 @@ export function useSubgraphOperations() {
|
||||
if (subgraphNodes.length === 0) {
|
||||
return
|
||||
}
|
||||
unpackNodes(subgraphNodes, true)
|
||||
doUnpack(subgraphNodes, true)
|
||||
}
|
||||
|
||||
const addSubgraphToLibrary = async () => {
|
||||
|
||||
@@ -64,8 +64,8 @@ function useVueNodeLifecycleIndividual() {
|
||||
|
||||
try {
|
||||
nodeManager.value.cleanup()
|
||||
} catch (error) {
|
||||
console.warn('Node manager cleanup failed:', error)
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
nodeManager.value = null
|
||||
}
|
||||
|
||||
@@ -32,12 +32,21 @@ const saveBrushToCache = debounce(function (key: string, brush: Brush): void {
|
||||
}
|
||||
}, 300)
|
||||
|
||||
/**
|
||||
* Loads brush settings from local storage.
|
||||
* @param key - The storage key.
|
||||
* @returns The brush settings object or null if not found.
|
||||
*/
|
||||
function loadBrushFromCache(key: string): Brush | null {
|
||||
try {
|
||||
const brushString = getStorageValue(key)
|
||||
if (brushString) return JSON.parse(brushString) as Brush
|
||||
return null
|
||||
} catch {
|
||||
if (brushString) {
|
||||
return JSON.parse(brushString) as Brush
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load brush from cache:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -211,12 +220,12 @@ export function useBrushDrawing(initialSettings?: {
|
||||
// Sync GPU on Undo/Redo
|
||||
watch(
|
||||
() => store.canvasHistory.currentStateIndex,
|
||||
() => {
|
||||
async () => {
|
||||
// Skip update if state was just saved
|
||||
if (isSavingHistory.value) return
|
||||
|
||||
// Update GPU textures to match restored canvas state
|
||||
updateGPUFromCanvas()
|
||||
await updateGPUFromCanvas()
|
||||
|
||||
// Clear preview to remove artifacts
|
||||
if (renderer && previewContext) {
|
||||
@@ -229,7 +238,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
|
||||
watch(
|
||||
() => store.gpuTexturesNeedRecreation,
|
||||
(needsRecreation) => {
|
||||
async (needsRecreation) => {
|
||||
if (
|
||||
!needsRecreation ||
|
||||
!device ||
|
||||
@@ -294,7 +303,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
)
|
||||
} else {
|
||||
// Fallback: read from canvas
|
||||
updateGPUFromCanvas()
|
||||
await updateGPUFromCanvas()
|
||||
}
|
||||
|
||||
// Update preview canvas if it exists
|
||||
@@ -417,7 +426,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
/**
|
||||
* Updates the GPU textures from the current canvas state.
|
||||
*/
|
||||
function updateGPUFromCanvas(): void {
|
||||
async function updateGPUFromCanvas(): Promise<void> {
|
||||
if (
|
||||
!device ||
|
||||
!maskTexture ||
|
||||
@@ -514,7 +523,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
})
|
||||
|
||||
// Upload initial data
|
||||
updateGPUFromCanvas()
|
||||
await updateGPUFromCanvas()
|
||||
|
||||
console.warn('✅ GPU resources initialized successfully')
|
||||
|
||||
@@ -801,7 +810,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
* Draws a point using the stroke processor for smoothing.
|
||||
* @param point - The point to draw.
|
||||
*/
|
||||
function drawWithBetterSmoothing(point: Point): void {
|
||||
async function drawWithBetterSmoothing(point: Point): Promise<void> {
|
||||
if (!strokeProcessor) return
|
||||
|
||||
// Process point to generate equidistant points
|
||||
@@ -955,7 +964,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
strokeProcessor = new StrokeProcessor(targetSpacing)
|
||||
|
||||
// Process first point
|
||||
drawWithBetterSmoothing(coords_canvas)
|
||||
await drawWithBetterSmoothing(coords_canvas)
|
||||
|
||||
smoothingLastDrawTime.value = new Date()
|
||||
} catch (error) {
|
||||
@@ -977,17 +986,18 @@ export function useBrushDrawing(initialSettings?: {
|
||||
const currentTool = store.currentTool
|
||||
|
||||
if (diff > 20 && !isDrawing.value) {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
if (!isDrawing.value) return // Fix: Prevent race condition
|
||||
try {
|
||||
initShape(CompositionOperation.SourceOver)
|
||||
gpuDrawPoint(coords_canvas)
|
||||
await gpuDrawPoint(coords_canvas)
|
||||
// smoothingCordsArray.value.push(coords_canvas) // Removed in favor of StrokeProcessor
|
||||
} catch (error) {
|
||||
console.error('[useBrushDrawing] Drawing error:', error)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
if (!isDrawing.value) return // Fix: Prevent race condition
|
||||
try {
|
||||
if (currentTool === 'eraser' || event.buttons === 2) {
|
||||
@@ -995,7 +1005,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
} else {
|
||||
initShape(CompositionOperation.SourceOver)
|
||||
}
|
||||
drawWithBetterSmoothing(coords_canvas)
|
||||
await drawWithBetterSmoothing(coords_canvas)
|
||||
} catch (error) {
|
||||
console.error('[useBrushDrawing] Drawing error:', error)
|
||||
}
|
||||
@@ -1108,7 +1118,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
* Starts the brush adjustment interaction.
|
||||
* @param event - The pointer event.
|
||||
*/
|
||||
function startBrushAdjustment(event: PointerEvent): void {
|
||||
async function startBrushAdjustment(event: PointerEvent): Promise<void> {
|
||||
event.preventDefault()
|
||||
|
||||
const coords = { x: event.offsetX, y: event.offsetY }
|
||||
@@ -1122,7 +1132,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
* Handles the brush adjustment movement.
|
||||
* @param event - The pointer event.
|
||||
*/
|
||||
function handleBrushAdjustment(event: PointerEvent): void {
|
||||
async function handleBrushAdjustment(event: PointerEvent): Promise<void> {
|
||||
if (!initialPoint.value) {
|
||||
return
|
||||
}
|
||||
@@ -1356,7 +1366,7 @@ export function useBrushDrawing(initialSettings?: {
|
||||
* @param point - The point to draw.
|
||||
* @param opacity - The opacity of the point.
|
||||
*/
|
||||
function gpuDrawPoint(point: Point, opacity: number = 1) {
|
||||
async function gpuDrawPoint(point: Point, opacity: number = 1) {
|
||||
if (renderer) {
|
||||
const width = store.maskCanvas!.width
|
||||
const height = store.maskCanvas!.height
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import type { CanvasLayers } from '@/composables/maskeditor/useCanvasHistory'
|
||||
import { useCanvasHistory } from '@/composables/maskeditor/useCanvasHistory'
|
||||
|
||||
// Define the store shape to avoid 'any' and cast to the expected type
|
||||
interface MaskEditorStoreState {
|
||||
maskCanvas: HTMLCanvasElement | null
|
||||
rgbCanvas: HTMLCanvasElement | null
|
||||
imgCanvas: HTMLCanvasElement | null
|
||||
maskCtx: CanvasRenderingContext2D | null
|
||||
rgbCtx: CanvasRenderingContext2D | null
|
||||
imgCtx: CanvasRenderingContext2D | null
|
||||
}
|
||||
|
||||
// Use vi.hoisted to create isolated mock state container
|
||||
const mockRefs = vi.hoisted(() => ({
|
||||
maskCanvas: null as HTMLCanvasElement | null,
|
||||
@@ -14,27 +22,49 @@ const mockRefs = vi.hoisted(() => ({
|
||||
imgCtx: null as CanvasRenderingContext2D | null
|
||||
}))
|
||||
|
||||
const mockLayers: CanvasLayers = {
|
||||
const mockStore: MaskEditorStoreState = {
|
||||
get maskCanvas() {
|
||||
return mockRefs.maskCanvas
|
||||
},
|
||||
set maskCanvas(val) {
|
||||
mockRefs.maskCanvas = val
|
||||
},
|
||||
get rgbCanvas() {
|
||||
return mockRefs.rgbCanvas
|
||||
},
|
||||
set rgbCanvas(val) {
|
||||
mockRefs.rgbCanvas = val
|
||||
},
|
||||
get imgCanvas() {
|
||||
return mockRefs.imgCanvas
|
||||
},
|
||||
set imgCanvas(val) {
|
||||
mockRefs.imgCanvas = val
|
||||
},
|
||||
get maskCtx() {
|
||||
return mockRefs.maskCtx
|
||||
},
|
||||
set maskCtx(val) {
|
||||
mockRefs.maskCtx = val
|
||||
},
|
||||
get rgbCtx() {
|
||||
return mockRefs.rgbCtx
|
||||
},
|
||||
set rgbCtx(val) {
|
||||
mockRefs.rgbCtx = val
|
||||
},
|
||||
get imgCtx() {
|
||||
return mockRefs.imgCtx
|
||||
},
|
||||
set imgCtx(val) {
|
||||
mockRefs.imgCtx = val
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@/stores/maskEditorStore', () => ({
|
||||
useMaskEditorStore: vi.fn(() => mockStore)
|
||||
}))
|
||||
|
||||
// Mock ImageBitmap using safe global augmentation pattern
|
||||
if (typeof globalThis.ImageBitmap === 'undefined') {
|
||||
globalThis.ImageBitmap = class ImageBitmap {
|
||||
@@ -110,14 +140,14 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with default values', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
expect(history.canUndo.value).toBe(false)
|
||||
expect(history.canRedo.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should save initial state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -143,7 +173,7 @@ describe('useCanvasHistory', () => {
|
||||
height: 0
|
||||
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
||||
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
history.saveInitialState()
|
||||
|
||||
expect(rafSpy).toHaveBeenCalled()
|
||||
@@ -159,7 +189,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
mockRefs.maskCtx = null
|
||||
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
history.saveInitialState()
|
||||
|
||||
expect(rafSpy).toHaveBeenCalled()
|
||||
@@ -183,7 +213,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('saveState', () => {
|
||||
it('should save a new state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
vi.mocked(mockRefs.maskCtx!.getImageData).mockClear()
|
||||
@@ -204,7 +234,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should clear redo states when saving new state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -219,7 +249,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should respect maxStates limit', () => {
|
||||
const history = useCanvasHistory(mockLayers, 3)
|
||||
const history = useCanvasHistory(3)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -239,7 +269,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should call saveInitialState if not initialized', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveState()
|
||||
|
||||
@@ -249,7 +279,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not save state if context is missing', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -269,7 +299,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('undo', () => {
|
||||
it('should undo to previous state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -284,7 +314,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not undo when no undo states available', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -300,7 +330,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should undo multiple times', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -318,7 +348,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not undo beyond first state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -339,7 +369,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('redo', () => {
|
||||
it('should redo to next state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -359,7 +389,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not redo when no redo states available', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -375,7 +405,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should redo multiple times', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -397,7 +427,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should not redo beyond last state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -419,7 +449,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('clearStates', () => {
|
||||
it('should clear all states', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -432,7 +462,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should allow saving initial state after clear', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.clearStates()
|
||||
@@ -451,13 +481,13 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('canUndo computed', () => {
|
||||
it('should be false with no states', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
expect(history.canUndo.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should be false with only initial state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -465,7 +495,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be true after saving a state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -474,7 +504,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be false after undoing to first state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -486,7 +516,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('canRedo computed', () => {
|
||||
it('should be false with no undo', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -495,7 +525,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be true after undo', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -505,7 +535,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be false after redo to last state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -516,7 +546,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should be false after saving new state', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -532,7 +562,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('restoreState', () => {
|
||||
it('should not restore if context is missing', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -553,7 +583,7 @@ describe('useCanvasHistory', () => {
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle rapid state saves', async () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
@@ -566,7 +596,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should handle maxStates of 1', () => {
|
||||
const history = useCanvasHistory(mockLayers, 1)
|
||||
const history = useCanvasHistory(1)
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -575,7 +605,7 @@ describe('useCanvasHistory', () => {
|
||||
})
|
||||
|
||||
it('should handle undo/redo cycling', () => {
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
history.saveState()
|
||||
@@ -599,7 +629,7 @@ describe('useCanvasHistory', () => {
|
||||
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
|
||||
}
|
||||
|
||||
const history = useCanvasHistory(mockLayers)
|
||||
const history = useCanvasHistory()
|
||||
|
||||
history.saveInitialState()
|
||||
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
||||
|
||||
// Define the state interface for better readability
|
||||
interface CanvasState {
|
||||
mask: ImageData | ImageBitmap
|
||||
rgb: ImageData | ImageBitmap
|
||||
img: ImageData | ImageBitmap
|
||||
}
|
||||
|
||||
export interface CanvasLayers {
|
||||
maskCanvas: HTMLCanvasElement | null
|
||||
maskCtx: CanvasRenderingContext2D | null
|
||||
rgbCanvas: HTMLCanvasElement | null
|
||||
rgbCtx: CanvasRenderingContext2D | null
|
||||
imgCanvas: HTMLCanvasElement | null
|
||||
imgCtx: CanvasRenderingContext2D | null
|
||||
}
|
||||
export function useCanvasHistory(maxStates = 20) {
|
||||
const store = useMaskEditorStore()
|
||||
|
||||
export function useCanvasHistory(layers: CanvasLayers, maxStates = 20) {
|
||||
const states = ref<CanvasState[]>([])
|
||||
const currentStateIndex = ref(-1)
|
||||
const initialized = ref(false)
|
||||
@@ -32,7 +27,7 @@ export function useCanvasHistory(layers: CanvasLayers, maxStates = 20) {
|
||||
})
|
||||
|
||||
const saveInitialState = () => {
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = layers
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = store
|
||||
|
||||
// Ensure all 3 contexts and canvases are ready
|
||||
if (
|
||||
@@ -84,7 +79,7 @@ export function useCanvasHistory(layers: CanvasLayers, maxStates = 20) {
|
||||
providedRgbData?: ImageData | ImageBitmap,
|
||||
providedImgData?: ImageData | ImageBitmap
|
||||
) => {
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = layers
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = store
|
||||
|
||||
if (
|
||||
!maskCtx ||
|
||||
@@ -149,7 +144,7 @@ export function useCanvasHistory(layers: CanvasLayers, maxStates = 20) {
|
||||
}
|
||||
|
||||
const restoreState = (state: CanvasState) => {
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = layers
|
||||
const { maskCtx, rgbCtx, imgCtx, maskCanvas, rgbCanvas, imgCanvas } = store
|
||||
if (
|
||||
!maskCtx ||
|
||||
!rgbCtx ||
|
||||
@@ -174,13 +169,13 @@ export function useCanvasHistory(layers: CanvasLayers, maxStates = 20) {
|
||||
imgCanvas.height = newHeight
|
||||
}
|
||||
|
||||
const canvasLayers = [
|
||||
const layers = [
|
||||
{ ctx: maskCtx, data: state.mask },
|
||||
{ ctx: rgbCtx, data: state.rgb },
|
||||
{ ctx: imgCtx, data: state.img }
|
||||
]
|
||||
|
||||
canvasLayers.forEach(({ ctx, data }) => {
|
||||
layers.forEach(({ ctx, data }) => {
|
||||
if (data instanceof ImageBitmap) {
|
||||
ctx.clearRect(0, 0, data.width, data.height)
|
||||
ctx.drawImage(data, 0, 0)
|
||||
|
||||
@@ -89,13 +89,13 @@ describe('useCanvasManager', () => {
|
||||
})
|
||||
|
||||
describe('invalidateCanvas', () => {
|
||||
it('should set canvas dimensions', () => {
|
||||
it('should set canvas dimensions', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.imgCanvas.width).toBe(512)
|
||||
expect(mockStore.imgCanvas.height).toBe(512)
|
||||
@@ -105,13 +105,13 @@ describe('useCanvasManager', () => {
|
||||
expect(mockStore.rgbCanvas.height).toBe(512)
|
||||
})
|
||||
|
||||
it('should draw original image', () => {
|
||||
it('should draw original image', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.imgCtx.drawImage).toHaveBeenCalledWith(
|
||||
origImage,
|
||||
@@ -122,14 +122,14 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should draw paint image when provided', () => {
|
||||
it('should draw paint image when provided', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
const paintImage = createMockImage(512, 512)
|
||||
|
||||
manager.invalidateCanvas(origImage, maskImage, paintImage)
|
||||
await manager.invalidateCanvas(origImage, maskImage, paintImage)
|
||||
|
||||
expect(mockStore.rgbCtx.drawImage).toHaveBeenCalledWith(
|
||||
paintImage,
|
||||
@@ -140,31 +140,31 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should not draw paint image when null', () => {
|
||||
it('should not draw paint image when null', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.rgbCtx.drawImage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should prepare mask', () => {
|
||||
it('should prepare mask', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.maskCtx.drawImage).toHaveBeenCalled()
|
||||
expect(mockStore.maskCtx.getImageData).toHaveBeenCalled()
|
||||
expect(mockStore.maskCtx.putImageData).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should throw error when canvas missing', () => {
|
||||
it('should throw error when canvas missing', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.imgCanvas = null! as HTMLCanvasElement
|
||||
@@ -172,12 +172,12 @@ describe('useCanvasManager', () => {
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
expect(() =>
|
||||
await expect(
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
).toThrow('Canvas elements or contexts not available')
|
||||
).rejects.toThrow('Canvas elements or contexts not available')
|
||||
})
|
||||
|
||||
it('should throw error when context missing', () => {
|
||||
it('should throw error when context missing', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.imgCtx = null! as CanvasRenderingContext2D
|
||||
@@ -185,20 +185,20 @@ describe('useCanvasManager', () => {
|
||||
const origImage = createMockImage(512, 512)
|
||||
const maskImage = createMockImage(512, 512)
|
||||
|
||||
expect(() =>
|
||||
await expect(
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
).toThrow('Canvas elements or contexts not available')
|
||||
).rejects.toThrow('Canvas elements or contexts not available')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateMaskColor', () => {
|
||||
it('should update mask color for black blend mode', () => {
|
||||
it('should update mask color for black blend mode', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskBlendMode = MaskBlendMode.Black
|
||||
mockStore.maskColor = { r: 0, g: 0, b: 0 }
|
||||
|
||||
manager.updateMaskColor()
|
||||
await manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCtx.fillStyle).toBe('rgb(0, 0, 0)')
|
||||
expect(mockStore.maskCanvas.style.mixBlendMode).toBe('initial')
|
||||
@@ -208,13 +208,13 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should update mask color for white blend mode', () => {
|
||||
it('should update mask color for white blend mode', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskBlendMode = MaskBlendMode.White
|
||||
mockStore.maskColor = { r: 255, g: 255, b: 255 }
|
||||
|
||||
manager.updateMaskColor()
|
||||
await manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCtx.fillStyle).toBe('rgb(255, 255, 255)')
|
||||
expect(mockStore.maskCanvas.style.mixBlendMode).toBe('initial')
|
||||
@@ -223,13 +223,13 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should update mask color for negative blend mode', () => {
|
||||
it('should update mask color for negative blend mode', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskBlendMode = MaskBlendMode.Negative
|
||||
mockStore.maskColor = { r: 255, g: 255, b: 255 }
|
||||
|
||||
manager.updateMaskColor()
|
||||
await manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCanvas.style.mixBlendMode).toBe('difference')
|
||||
expect(mockStore.maskCanvas.style.opacity).toBe('1')
|
||||
@@ -238,14 +238,14 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should update all pixels with mask color', () => {
|
||||
it('should update all pixels with mask color', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskColor = { r: 128, g: 64, b: 32 }
|
||||
mockStore.maskCanvas.width = 100
|
||||
mockStore.maskCanvas.height = 100
|
||||
|
||||
manager.updateMaskColor()
|
||||
await manager.updateMaskColor()
|
||||
|
||||
for (let i = 0; i < mockImageData.data.length; i += 4) {
|
||||
expect(mockImageData.data[i]).toBe(128)
|
||||
@@ -260,39 +260,39 @@ describe('useCanvasManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should return early when canvas missing', () => {
|
||||
it('should return early when canvas missing', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskCanvas = null! as HTMLCanvasElement
|
||||
|
||||
manager.updateMaskColor()
|
||||
await manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCtx.getImageData).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return early when context missing', () => {
|
||||
it('should return early when context missing', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskCtx = null! as CanvasRenderingContext2D
|
||||
|
||||
manager.updateMaskColor()
|
||||
await manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.canvasBackground.style.backgroundColor).toBe('')
|
||||
})
|
||||
|
||||
it('should handle different opacity values', () => {
|
||||
it('should handle different opacity values', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskOpacity = 0.5
|
||||
|
||||
manager.updateMaskColor()
|
||||
await manager.updateMaskColor()
|
||||
|
||||
expect(mockStore.maskCanvas.style.opacity).toBe('0.5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepareMask', () => {
|
||||
it('should invert mask alpha', () => {
|
||||
it('should invert mask alpha', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
for (let i = 0; i < mockImageData.data.length; i += 4) {
|
||||
@@ -302,14 +302,14 @@ describe('useCanvasManager', () => {
|
||||
const origImage = createMockImage(100, 100)
|
||||
const maskImage = createMockImage(100, 100)
|
||||
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
for (let i = 0; i < mockImageData.data.length; i += 4) {
|
||||
expect(mockImageData.data[i + 3]).toBe(127)
|
||||
}
|
||||
})
|
||||
|
||||
it('should apply mask color to all pixels', () => {
|
||||
it('should apply mask color to all pixels', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
mockStore.maskColor = { r: 100, g: 150, b: 200 }
|
||||
@@ -317,7 +317,7 @@ describe('useCanvasManager', () => {
|
||||
const origImage = createMockImage(100, 100)
|
||||
const maskImage = createMockImage(100, 100)
|
||||
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
for (let i = 0; i < mockImageData.data.length; i += 4) {
|
||||
expect(mockImageData.data[i]).toBe(100)
|
||||
@@ -326,13 +326,13 @@ describe('useCanvasManager', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should set composite operation', () => {
|
||||
it('should set composite operation', async () => {
|
||||
const manager = useCanvasManager()
|
||||
|
||||
const origImage = createMockImage(100, 100)
|
||||
const maskImage = createMockImage(100, 100)
|
||||
|
||||
manager.invalidateCanvas(origImage, maskImage, null)
|
||||
await manager.invalidateCanvas(origImage, maskImage, null)
|
||||
|
||||
expect(mockStore.maskCtx.globalCompositeOperation).toBe('source-over')
|
||||
})
|
||||
|
||||
@@ -5,11 +5,11 @@ import { MaskBlendMode } from '@/extensions/core/maskeditor/types'
|
||||
export function useCanvasManager() {
|
||||
const store = useMaskEditorStore()
|
||||
|
||||
const prepareMask = (
|
||||
const prepareMask = async (
|
||||
image: HTMLImageElement,
|
||||
maskCanvasEl: HTMLCanvasElement,
|
||||
maskContext: CanvasRenderingContext2D
|
||||
): void => {
|
||||
): Promise<void> => {
|
||||
const maskColor = store.maskColor
|
||||
|
||||
maskContext.drawImage(image, 0, 0, maskCanvasEl.width, maskCanvasEl.height)
|
||||
@@ -33,11 +33,11 @@ export function useCanvasManager() {
|
||||
maskContext.putImageData(maskData, 0, 0)
|
||||
}
|
||||
|
||||
const invalidateCanvas = (
|
||||
const invalidateCanvas = async (
|
||||
origImage: HTMLImageElement,
|
||||
maskImage: HTMLImageElement,
|
||||
paintImage: HTMLImageElement | null
|
||||
): void => {
|
||||
): Promise<void> => {
|
||||
const { imgCanvas, maskCanvas, rgbCanvas, imgCtx, maskCtx, rgbCtx } = store
|
||||
|
||||
if (
|
||||
@@ -64,7 +64,7 @@ export function useCanvasManager() {
|
||||
rgbCtx.drawImage(paintImage, 0, 0, paintImage.width, paintImage.height)
|
||||
}
|
||||
|
||||
prepareMask(maskImage, maskCanvas, maskCtx)
|
||||
await prepareMask(maskImage, maskCanvas, maskCtx)
|
||||
}
|
||||
|
||||
const setCanvasBackground = (): void => {
|
||||
@@ -81,7 +81,7 @@ export function useCanvasManager() {
|
||||
}
|
||||
}
|
||||
|
||||
const updateMaskColor = (): void => {
|
||||
const updateMaskColor = async (): Promise<void> => {
|
||||
const { maskCanvas, maskCtx, maskColor, maskBlendMode, maskOpacity } = store
|
||||
|
||||
if (!maskCanvas || !maskCtx) return
|
||||
|
||||
@@ -118,7 +118,10 @@ export function useCanvasTransform() {
|
||||
* Recreates and updates GPU textures after transformation
|
||||
* This is required because GPU textures have immutable dimensions
|
||||
*/
|
||||
const recreateGPUTextures = (width: number, height: number): void => {
|
||||
const recreateGPUTextures = async (
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<void> => {
|
||||
if (
|
||||
!store.tgpuRoot ||
|
||||
!store.maskCanvas ||
|
||||
@@ -178,7 +181,7 @@ export function useCanvasTransform() {
|
||||
/**
|
||||
* Rotates all canvas layers 90 degrees clockwise and updates GPU
|
||||
*/
|
||||
const rotateClockwise = (): void => {
|
||||
const rotateClockwise = async (): Promise<void> => {
|
||||
const { maskCanvas, maskCtx, rgbCanvas, rgbCtx, imgCanvas, imgCtx } = store
|
||||
|
||||
if (
|
||||
@@ -217,7 +220,7 @@ export function useCanvasTransform() {
|
||||
|
||||
// Recreate GPU textures with new dimensions if GPU is active
|
||||
if (store.tgpuRoot) {
|
||||
recreateGPUTextures(origHeight, origWidth)
|
||||
await recreateGPUTextures(origHeight, origWidth)
|
||||
}
|
||||
|
||||
// Save to history
|
||||
@@ -227,7 +230,7 @@ export function useCanvasTransform() {
|
||||
/**
|
||||
* Rotates all canvas layers 90 degrees counter-clockwise and updates GPU
|
||||
*/
|
||||
const rotateCounterclockwise = (): void => {
|
||||
const rotateCounterclockwise = async (): Promise<void> => {
|
||||
const { maskCanvas, maskCtx, rgbCanvas, rgbCtx, imgCanvas, imgCtx } = store
|
||||
|
||||
if (
|
||||
@@ -266,7 +269,7 @@ export function useCanvasTransform() {
|
||||
|
||||
// Recreate GPU textures with new dimensions if GPU is active
|
||||
if (store.tgpuRoot) {
|
||||
recreateGPUTextures(origHeight, origWidth)
|
||||
await recreateGPUTextures(origHeight, origWidth)
|
||||
}
|
||||
|
||||
// Save to history
|
||||
@@ -276,7 +279,7 @@ export function useCanvasTransform() {
|
||||
/**
|
||||
* Mirrors all canvas layers horizontally and updates GPU
|
||||
*/
|
||||
const mirrorHorizontal = (): void => {
|
||||
const mirrorHorizontal = async (): Promise<void> => {
|
||||
const { maskCanvas, maskCtx, rgbCanvas, rgbCtx, imgCanvas, imgCtx } = store
|
||||
|
||||
if (
|
||||
@@ -303,7 +306,7 @@ export function useCanvasTransform() {
|
||||
|
||||
// Update GPU textures if GPU is active (dimensions unchanged, just data)
|
||||
if (store.tgpuRoot) {
|
||||
recreateGPUTextures(maskCanvas.width, maskCanvas.height)
|
||||
await recreateGPUTextures(maskCanvas.width, maskCanvas.height)
|
||||
}
|
||||
|
||||
// Save to history
|
||||
@@ -313,7 +316,7 @@ export function useCanvasTransform() {
|
||||
/**
|
||||
* Mirrors all canvas layers vertically and updates GPU
|
||||
*/
|
||||
const mirrorVertical = (): void => {
|
||||
const mirrorVertical = async (): Promise<void> => {
|
||||
const { maskCanvas, maskCtx, rgbCanvas, rgbCtx, imgCanvas, imgCtx } = store
|
||||
|
||||
if (
|
||||
@@ -340,7 +343,7 @@ export function useCanvasTransform() {
|
||||
|
||||
// Update GPU textures if GPU is active (dimensions unchanged, just data)
|
||||
if (store.tgpuRoot) {
|
||||
recreateGPUTextures(maskCanvas.width, maskCanvas.height)
|
||||
await recreateGPUTextures(maskCanvas.width, maskCanvas.height)
|
||||
}
|
||||
|
||||
// Save to history
|
||||
|
||||
@@ -35,9 +35,13 @@ function useImageLoaderInternal() {
|
||||
|
||||
store.image = baseImage
|
||||
|
||||
canvasManager.invalidateCanvas(baseImage, maskImage, paintImage || null)
|
||||
await canvasManager.invalidateCanvas(
|
||||
baseImage,
|
||||
maskImage,
|
||||
paintImage || null
|
||||
)
|
||||
|
||||
canvasManager.updateMaskColor()
|
||||
await canvasManager.updateMaskColor()
|
||||
|
||||
return baseImage
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
// Private layer filename functions
|
||||
interface ImageLayerFilenames {
|
||||
maskedImage: string
|
||||
paint: string
|
||||
@@ -29,32 +30,6 @@ function imageLayerFilenamesByTimestamp(
|
||||
}
|
||||
}
|
||||
|
||||
function getContext2D(canvas: HTMLCanvasElement): CanvasRenderingContext2D {
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Failed to get 2D rendering context')
|
||||
return ctx
|
||||
}
|
||||
|
||||
function applyInvertedMaskAlpha(
|
||||
targetCtx: CanvasRenderingContext2D,
|
||||
maskCanvas: HTMLCanvasElement
|
||||
): void {
|
||||
const maskCtx = getContext2D(maskCanvas)
|
||||
const maskData = maskCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
maskCanvas.width,
|
||||
maskCanvas.height
|
||||
)
|
||||
|
||||
const { width, height } = targetCtx.canvas
|
||||
const imageData = targetCtx.getImageData(0, 0, width, height)
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
imageData.data[i + 3] = 255 - maskData.data[i + 3]
|
||||
}
|
||||
targetCtx.putImageData(imageData, 0, 0)
|
||||
}
|
||||
|
||||
export function useMaskEditorSaver() {
|
||||
const dataStore = useMaskEditorDataStore()
|
||||
const editorStore = useMaskEditorStore()
|
||||
@@ -124,10 +99,31 @@ export function useMaskEditorSaver() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = imgCanvas.width
|
||||
canvas.height = imgCanvas.height
|
||||
const ctx = getContext2D(canvas)
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
ctx.drawImage(imgCanvas, 0, 0)
|
||||
applyInvertedMaskAlpha(ctx, maskCanvas)
|
||||
|
||||
const maskCtx = maskCanvas.getContext('2d')!
|
||||
const maskData = maskCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
maskCanvas.width,
|
||||
maskCanvas.height
|
||||
)
|
||||
|
||||
const refinedMaskData = new Uint8ClampedArray(maskData.data.length)
|
||||
for (let i = 0; i < maskData.data.length; i += 4) {
|
||||
refinedMaskData[i] = 0
|
||||
refinedMaskData[i + 1] = 0
|
||||
refinedMaskData[i + 2] = 0
|
||||
refinedMaskData[i + 3] = 255 - maskData.data[i + 3]
|
||||
}
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
imageData.data[i + 3] = refinedMaskData[i + 3]
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
|
||||
const blob = await canvasToBlob(canvas)
|
||||
const ref = createFileRef(filename)
|
||||
@@ -154,9 +150,11 @@ export function useMaskEditorSaver() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = imgCanvas.width
|
||||
canvas.height = imgCanvas.height
|
||||
const ctx = getContext2D(canvas)
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
ctx.drawImage(imgCanvas, 0, 0)
|
||||
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
ctx.drawImage(paintCanvas, 0, 0)
|
||||
|
||||
const blob = await canvasToBlob(canvas)
|
||||
@@ -174,11 +172,34 @@ export function useMaskEditorSaver() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = imgCanvas.width
|
||||
canvas.height = imgCanvas.height
|
||||
const ctx = getContext2D(canvas)
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
ctx.drawImage(imgCanvas, 0, 0)
|
||||
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
ctx.drawImage(paintCanvas, 0, 0)
|
||||
applyInvertedMaskAlpha(ctx, maskCanvas)
|
||||
|
||||
const maskCtx = maskCanvas.getContext('2d')!
|
||||
const maskData = maskCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
maskCanvas.width,
|
||||
maskCanvas.height
|
||||
)
|
||||
|
||||
const refinedMaskData = new Uint8ClampedArray(maskData.data.length)
|
||||
for (let i = 0; i < maskData.data.length; i += 4) {
|
||||
refinedMaskData[i] = 0
|
||||
refinedMaskData[i + 1] = 0
|
||||
refinedMaskData[i + 2] = 0
|
||||
refinedMaskData[i + 3] = 255 - maskData.data[i + 3]
|
||||
}
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
imageData.data[i + 3] = refinedMaskData[i + 3]
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
|
||||
const blob = await canvasToBlob(canvas)
|
||||
const ref = createFileRef(filename)
|
||||
@@ -189,26 +210,16 @@ export function useMaskEditorSaver() {
|
||||
async function uploadAllLayers(outputData: EditorOutputData): Promise<void> {
|
||||
const sourceRef = dataStore.inputData!.sourceRef
|
||||
|
||||
const actualMaskedRef = await uploadLayer(
|
||||
outputData.maskedImage,
|
||||
sourceRef,
|
||||
'/upload/mask'
|
||||
)
|
||||
const actualPaintRef = await uploadLayer(
|
||||
outputData.paintLayer,
|
||||
sourceRef,
|
||||
'/upload/image'
|
||||
)
|
||||
const actualPaintedRef = await uploadLayer(
|
||||
const actualMaskedRef = await uploadMask(outputData.maskedImage, sourceRef)
|
||||
const actualPaintRef = await uploadImage(outputData.paintLayer, sourceRef)
|
||||
const actualPaintedRef = await uploadImage(
|
||||
outputData.paintedImage,
|
||||
sourceRef,
|
||||
'/upload/image'
|
||||
sourceRef
|
||||
)
|
||||
|
||||
const actualPaintedMaskedRef = await uploadLayer(
|
||||
const actualPaintedMaskedRef = await uploadMask(
|
||||
outputData.paintedMaskedImage,
|
||||
actualPaintedRef,
|
||||
'/upload/mask'
|
||||
actualPaintedRef
|
||||
)
|
||||
|
||||
outputData.maskedImage.ref = actualMaskedRef
|
||||
@@ -217,10 +228,9 @@ export function useMaskEditorSaver() {
|
||||
outputData.paintedMaskedImage.ref = actualPaintedMaskedRef
|
||||
}
|
||||
|
||||
async function uploadLayer(
|
||||
async function uploadMask(
|
||||
layer: EditorOutputLayer,
|
||||
originalRef: ImageRef,
|
||||
endpoint: '/upload/mask' | '/upload/image'
|
||||
originalRef: ImageRef
|
||||
): Promise<ImageRef> {
|
||||
const formData = new FormData()
|
||||
formData.append('image', layer.blob, layer.ref.filename)
|
||||
@@ -228,22 +238,61 @@ export function useMaskEditorSaver() {
|
||||
formData.append('type', 'input')
|
||||
formData.append('subfolder', 'clipspace')
|
||||
|
||||
const response = await api.fetchApi(endpoint, {
|
||||
const response = await api.fetchApi('/upload/mask', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload to ${endpoint}: ${layer.ref.filename}`)
|
||||
throw new Error(`Failed to upload mask: ${layer.ref.filename}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data?.name) {
|
||||
return {
|
||||
filename: data.name,
|
||||
subfolder: data.subfolder || layer.ref.subfolder,
|
||||
type: data.type || layer.ref.type
|
||||
try {
|
||||
const data = await response.json()
|
||||
if (data?.name) {
|
||||
return {
|
||||
filename: data.name,
|
||||
subfolder: data.subfolder || layer.ref.subfolder,
|
||||
type: data.type || layer.ref.type
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MaskEditorSaver] Failed to parse upload response:', error)
|
||||
}
|
||||
|
||||
return layer.ref
|
||||
}
|
||||
|
||||
async function uploadImage(
|
||||
layer: EditorOutputLayer,
|
||||
originalRef: ImageRef
|
||||
): Promise<ImageRef> {
|
||||
const formData = new FormData()
|
||||
formData.append('image', layer.blob, layer.ref.filename)
|
||||
formData.append('original_ref', JSON.stringify(originalRef))
|
||||
formData.append('type', 'input')
|
||||
formData.append('subfolder', 'clipspace')
|
||||
|
||||
const response = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload image: ${layer.ref.filename}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await response.json()
|
||||
if (data?.name) {
|
||||
return {
|
||||
filename: data.name,
|
||||
subfolder: data.subfolder || layer.ref.subfolder,
|
||||
type: data.type || layer.ref.type
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MaskEditorSaver] Failed to parse upload response:', error)
|
||||
}
|
||||
|
||||
return layer.ref
|
||||
@@ -324,7 +373,7 @@ export function useMaskEditorSaver() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = source.width
|
||||
canvas.height = source.height
|
||||
const ctx = getContext2D(canvas)
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.drawImage(source, 0, 0)
|
||||
return canvas
|
||||
}
|
||||
|
||||
@@ -59,9 +59,14 @@ export function usePanAndZoom() {
|
||||
store.canvasHistory.undo()
|
||||
}
|
||||
|
||||
const invalidatePanZoom = (): void => {
|
||||
const invalidatePanZoom = async (): Promise<void> => {
|
||||
// Single validation check upfront
|
||||
if (!image.value?.width || !image.value?.height || !zoom_ratio.value) {
|
||||
if (
|
||||
!image.value?.width ||
|
||||
!image.value?.height ||
|
||||
!pan_offset.value ||
|
||||
!zoom_ratio.value
|
||||
) {
|
||||
console.warn('Missing required properties for pan/zoom')
|
||||
return
|
||||
}
|
||||
@@ -108,7 +113,7 @@ export function usePanAndZoom() {
|
||||
initialPan.value = { ...pan_offset.value }
|
||||
}
|
||||
|
||||
const handlePanMove = (event: PointerEvent): void => {
|
||||
const handlePanMove = async (event: PointerEvent): Promise<void> => {
|
||||
if (mouseDownPoint.value === null) {
|
||||
throw new Error('mouseDownPoint is null')
|
||||
}
|
||||
@@ -121,10 +126,10 @@ export function usePanAndZoom() {
|
||||
|
||||
pan_offset.value = { x: pan_x, y: pan_y }
|
||||
|
||||
invalidatePanZoom()
|
||||
await invalidatePanZoom()
|
||||
}
|
||||
|
||||
const handleSingleTouchPan = (touch: Touch): void => {
|
||||
const handleSingleTouchPan = async (touch: Touch): Promise<void> => {
|
||||
if (lastTouchPoint.value === null) {
|
||||
lastTouchPoint.value = { x: touch.clientX, y: touch.clientY }
|
||||
return
|
||||
@@ -136,7 +141,7 @@ export function usePanAndZoom() {
|
||||
pan_offset.value.x += deltaX
|
||||
pan_offset.value.y += deltaY
|
||||
|
||||
invalidatePanZoom()
|
||||
await invalidatePanZoom()
|
||||
|
||||
lastTouchPoint.value = { x: touch.clientX, y: touch.clientY }
|
||||
}
|
||||
@@ -170,7 +175,7 @@ export function usePanAndZoom() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTouchMove = (event: TouchEvent): void => {
|
||||
const handleTouchMove = async (event: TouchEvent): Promise<void> => {
|
||||
event.preventDefault()
|
||||
|
||||
if (penPointerIdList.value.length > 0) return
|
||||
@@ -210,11 +215,11 @@ export function usePanAndZoom() {
|
||||
pan_offset.value.x += touchX - touchX * scaleFactor
|
||||
pan_offset.value.y += touchY - touchY * scaleFactor
|
||||
|
||||
invalidatePanZoom()
|
||||
await invalidatePanZoom()
|
||||
lastTouchZoomDistance.value = newDistance
|
||||
lastTouchMidPoint.value = midpoint
|
||||
} else if (event.touches.length === 1) {
|
||||
handleSingleTouchPan(event.touches[0])
|
||||
await handleSingleTouchPan(event.touches[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +239,7 @@ export function usePanAndZoom() {
|
||||
}
|
||||
}
|
||||
|
||||
const zoom = (event: WheelEvent): void => {
|
||||
const zoom = async (event: WheelEvent): Promise<void> => {
|
||||
const cursorPosition = { x: event.clientX, y: event.clientY }
|
||||
|
||||
const oldZoom = zoom_ratio.value
|
||||
@@ -258,7 +263,7 @@ export function usePanAndZoom() {
|
||||
pan_offset.value.x += mouseX - mouseX * scaleFactor
|
||||
pan_offset.value.y += mouseY - mouseY * scaleFactor
|
||||
|
||||
invalidatePanZoom()
|
||||
await invalidatePanZoom()
|
||||
|
||||
const newImageWidth = maskCanvas.value.clientWidth
|
||||
|
||||
@@ -282,7 +287,7 @@ export function usePanAndZoom() {
|
||||
return { sidePanelWidth, toolPanelWidth }
|
||||
}
|
||||
|
||||
const smoothResetView = (duration: number = 500): void => {
|
||||
const smoothResetView = async (duration: number = 500): Promise<void> => {
|
||||
if (!image.value || !rootElement.value) return
|
||||
|
||||
const startZoom = zoom_ratio.value
|
||||
@@ -316,7 +321,7 @@ export function usePanAndZoom() {
|
||||
}
|
||||
|
||||
const startTime = performance.now()
|
||||
const animate = (currentTime: number) => {
|
||||
const animate = async (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const eased = 1 - Math.pow(1 - progress, 3)
|
||||
@@ -325,7 +330,7 @@ export function usePanAndZoom() {
|
||||
pan_offset.value.x = startPan.x + (targetPan.x - startPan.x) * eased
|
||||
pan_offset.value.y = startPan.y + (targetPan.y - startPan.y) * eased
|
||||
|
||||
invalidatePanZoom()
|
||||
await invalidatePanZoom()
|
||||
|
||||
const interpolatedRatio = startZoom + (1.0 - startZoom) * eased
|
||||
store.displayZoomRatio = interpolatedRatio
|
||||
@@ -339,12 +344,12 @@ export function usePanAndZoom() {
|
||||
interpolatedZoomRatio.value = 1.0
|
||||
}
|
||||
|
||||
const initializeCanvasPanZoom = (
|
||||
const initializeCanvasPanZoom = async (
|
||||
img: HTMLImageElement,
|
||||
root: HTMLElement,
|
||||
toolPanel?: HTMLElement | null,
|
||||
sidePanel?: HTMLElement | null
|
||||
): void => {
|
||||
): Promise<void> => {
|
||||
rootElement.value = root
|
||||
toolPanelElement.value = toolPanel || null
|
||||
sidePanelElement.value = sidePanel || null
|
||||
@@ -386,14 +391,14 @@ export function usePanAndZoom() {
|
||||
|
||||
penPointerIdList.value = []
|
||||
|
||||
invalidatePanZoom()
|
||||
await invalidatePanZoom()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => store.resetZoomTrigger,
|
||||
() => {
|
||||
async () => {
|
||||
if (interpolatedZoomRatio.value === 1) return
|
||||
smoothResetView()
|
||||
await smoothResetView()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ export function useToolManager(
|
||||
const isSpacePressed = keyboard.isKeyDown(' ')
|
||||
|
||||
if (event.buttons === 4 || (event.buttons === 1 && isSpacePressed)) {
|
||||
panZoom.handlePanMove(event)
|
||||
await panZoom.handlePanMove(event)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -331,6 +331,9 @@ export const formatPricingResult = (
|
||||
}
|
||||
|
||||
if (!isPricingResult(result)) {
|
||||
if (result !== undefined && result !== null) {
|
||||
console.warn('[pricing/jsonata] invalid result format:', result)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
@@ -46,13 +46,16 @@ export const useComputedWithWidgetWatch = (
|
||||
) => {
|
||||
const { widgetNames, triggerCanvasRedraw = false } = options
|
||||
|
||||
// Create a reactive trigger based on widget values
|
||||
const widgetValues = ref<Record<string, unknown>>({})
|
||||
|
||||
// Initialize widget observers
|
||||
if (node.widgets) {
|
||||
const widgetsToObserve = widgetNames
|
||||
? node.widgets.filter((widget) => widgetNames.includes(widget.name))
|
||||
: node.widgets
|
||||
|
||||
// Initialize current values
|
||||
const currentValues: Record<string, unknown> = {}
|
||||
widgetsToObserve.forEach((widget) => {
|
||||
currentValues[widget.name] = widget.value
|
||||
@@ -61,17 +64,20 @@ export const useComputedWithWidgetWatch = (
|
||||
|
||||
widgetsToObserve.forEach((widget) => {
|
||||
widget.callback = useChainCallback(widget.callback, () => {
|
||||
// Update the reactive widget values
|
||||
widgetValues.value = {
|
||||
...widgetValues.value,
|
||||
[widget.name]: widget.value
|
||||
}
|
||||
|
||||
// Optionally trigger a canvas redraw
|
||||
if (triggerCanvasRedraw) {
|
||||
node.graph?.setDirtyCanvas(true, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
if (widgetNames && widgetNames.length > widgetsToObserve.length) {
|
||||
//Inputs have been included
|
||||
const indexesToObserve = widgetNames
|
||||
.map((name) =>
|
||||
widgetsToObserve.some((w) => w.name == name)
|
||||
@@ -95,6 +101,8 @@ export const useComputedWithWidgetWatch = (
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function that creates a computed that responds to widget changes.
|
||||
// The computed will be re-evaluated whenever any observed widget changes.
|
||||
return <T>(computeFn: () => T): ComputedRef<T> => {
|
||||
return computedWithControl(widgetValues, computeFn)
|
||||
}
|
||||
|
||||