Compare commits
92 Commits
v1.32.9
...
feat/remot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d314172b98 | ||
|
|
6c3408592e | ||
|
|
b6632443dc | ||
|
|
c8a1df3a05 | ||
|
|
39204135ba | ||
|
|
ffa55cb92b | ||
|
|
3856e0deea | ||
|
|
7b589b5502 | ||
|
|
72a2581068 | ||
|
|
22aea29a0d | ||
|
|
637c1995b4 | ||
|
|
550ca0c911 | ||
|
|
896867b03c | ||
|
|
0cd0218946 | ||
|
|
334404aa3b | ||
|
|
31d842217b | ||
|
|
3dd3e26003 | ||
|
|
e6332046b0 | ||
|
|
5fa76e23d9 | ||
|
|
fcfb5437a9 | ||
|
|
5ff3a0ed52 | ||
|
|
5fa0295ff5 | ||
|
|
1348a0934a | ||
|
|
01f8e77251 | ||
|
|
31c03b669e | ||
|
|
c9da19b5b5 | ||
|
|
10222860eb | ||
|
|
4597b7e600 | ||
|
|
6782d04f00 | ||
|
|
4bb5c12fac | ||
|
|
8b5cfe7e55 | ||
|
|
135169003f | ||
|
|
d58a464c9c | ||
|
|
e54b972550 | ||
|
|
9bd63dbe6a | ||
|
|
a21c813d11 | ||
|
|
df373af987 | ||
|
|
c06a7279e2 | ||
|
|
ffc17c054b | ||
|
|
ddb00d02d5 | ||
|
|
4815d6b14c | ||
|
|
a9653ba9c7 | ||
|
|
30bafcd019 | ||
|
|
b789791fd9 | ||
|
|
723f53751e | ||
|
|
2539a7d2ce | ||
|
|
c9556d7aff | ||
|
|
58b051a473 | ||
|
|
0b33470744 | ||
|
|
fb3ce74d2f | ||
|
|
86d3f0ebd5 | ||
|
|
09c888e338 | ||
|
|
6d41e8b6e4 | ||
|
|
274f77869b | ||
|
|
f5c9f69678 | ||
|
|
c1e237255a | ||
|
|
a91b9f288f | ||
|
|
4adcf09cca | ||
|
|
1dbb3fc1b9 | ||
|
|
d6c5b33fce | ||
|
|
f5608435b4 | ||
|
|
9da82f47ef | ||
|
|
a8d6f7baff | ||
|
|
a7daa5071c | ||
|
|
639975b804 | ||
|
|
ff0d385db8 | ||
|
|
6e2e591937 | ||
|
|
27fcc4554f | ||
|
|
e563c1be75 | ||
|
|
5a297e520c | ||
|
|
85bec5ee47 | ||
|
|
bdf6d4dea2 | ||
|
|
b8a796212c | ||
|
|
bc553f12be | ||
|
|
6bb35d46c1 | ||
|
|
68c38f0098 | ||
|
|
236247f05f | ||
|
|
87d6d18c57 | ||
|
|
87106ccb95 | ||
|
|
a20fb7d260 | ||
|
|
836cd7f9ba | ||
|
|
acd855601c | ||
|
|
423a2e76bc | ||
|
|
26578981d4 | ||
|
|
38fb53dca8 | ||
|
|
a832141a45 | ||
|
|
d1f0211b61 | ||
|
|
cc42c2967c | ||
|
|
bb51a5aa76 | ||
|
|
674d884e79 | ||
|
|
6f89d9a9f8 | ||
|
|
08b206f191 |
@@ -1,5 +1,5 @@
|
||||
# Description: When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
name: 'Api: Update Electron API Types'
|
||||
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Description: When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
name: 'Api: Update Manager API Types'
|
||||
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Description: When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
name: 'Api: Update Registry API Types'
|
||||
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
|
||||
2
.github/workflows/ci-json-validation.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq
|
||||
name: "CI: JSON Validation"
|
||||
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
2
.github/workflows/ci-lint-format.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Linting and code formatting validation for pull requests
|
||||
name: "CI: Lint Format"
|
||||
description: "Linting and code formatting validation for pull requests"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
2
.github/workflows/ci-python-validation.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Validates Python code in tools/devtools directory
|
||||
name: "CI: Python Validation"
|
||||
description: "Validates Python code in tools/devtools directory"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
2
.github/workflows/ci-tests-e2e-forks.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Deploys test results from forked PRs (forks can't access deployment secrets)
|
||||
name: "CI: Tests E2E (Deploy for Forks)"
|
||||
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
|
||||
2
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages
|
||||
name: "CI: Tests E2E"
|
||||
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Description: Deploys Storybook previews from forked PRs (forks can't access deployment secrets)
|
||||
name: "CI: Tests Storybook (Deploy for Forks)"
|
||||
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
|
||||
3
.github/workflows/ci-tests-storybook.yaml
vendored
@@ -1,10 +1,9 @@
|
||||
# Description: Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages
|
||||
name: "CI: Tests Storybook"
|
||||
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
# Post starting comment for non-forked PRs
|
||||
|
||||
2
.github/workflows/ci-tests-unit.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Unit and component testing with Vitest
|
||||
name: "CI: Tests Unit"
|
||||
description: "Unit and component testing with Vitest"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
2
.github/workflows/ci-yaml-validation.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Validates YAML syntax and style using yamllint with relaxed rules
|
||||
name: "CI: YAML Validation"
|
||||
description: "Validates YAML syntax and style using yamllint with relaxed rules"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
69
.github/workflows/cloud-backport-tag.yaml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: Cloud Backport Tag
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ['closed']
|
||||
branches: [cloud/*]
|
||||
|
||||
jobs:
|
||||
create-tag:
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'backport')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout merge commit
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Create tag for cloud backport
|
||||
id: tag
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
if [[ ! "$BRANCH" =~ ^cloud/([0-9]+)\.([0-9]+)$ ]]; then
|
||||
echo "::error::Base branch '$BRANCH' is not a cloud/x.y branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MAJOR="${BASH_REMATCH[1]}"
|
||||
MINOR="${BASH_REMATCH[2]}"
|
||||
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
if [[ "$VERSION" =~ ^${MAJOR}\.${MINOR}\.([0-9]+)(-.+)?$ ]]; then
|
||||
PATCH="${BASH_REMATCH[1]}"
|
||||
SUFFIX="${BASH_REMATCH[2]:-}"
|
||||
else
|
||||
echo "::error::Version '${VERSION}' does not match cloud branch '${BRANCH}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="cloud/v${VERSION}"
|
||||
|
||||
if git ls-remote --tags origin "${TAG}" | grep -Fq "refs/tags/${TAG}"; then
|
||||
echo "::notice::Tag ${TAG} already exists; skipping"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git tag "${TAG}" "${{ github.event.pull_request.merge_commit_sha }}"
|
||||
git push origin "${TAG}"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "Created tag: ${TAG}"
|
||||
echo "Branch: ${BRANCH}"
|
||||
echo "Version: ${VERSION}"
|
||||
echo "Commit: ${{ github.event.pull_request.merge_commit_sha }}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
2
.github/workflows/i18n-update-core.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Generates and updates translations for core ComfyUI components using OpenAI
|
||||
name: "i18n: Update Core"
|
||||
description: "Generates and updates translations for core ComfyUI components using OpenAI"
|
||||
|
||||
on:
|
||||
# Manual dispatch for urgent translation updates
|
||||
|
||||
17
.github/workflows/pr-backport.yaml
vendored
@@ -78,8 +78,7 @@ jobs:
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
|
||||
else
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
LABELS=$(echo "$LABELS" | jq -r '.[].name')
|
||||
LABELS=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
add_target() {
|
||||
@@ -237,8 +236,8 @@ jobs:
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
|
||||
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
|
||||
@@ -327,8 +326,8 @@ jobs:
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
|
||||
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
@@ -365,9 +364,9 @@ jobs:
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
PR_NUMBER=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")
|
||||
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
|
||||
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
|
||||
2
.github/workflows/pr-claude-review.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: AI-powered code review triggered by adding the 'claude-review' label to a PR
|
||||
name: "PR: Claude Review"
|
||||
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
4
.github/workflows/release-branch-create.yaml
vendored
@@ -148,10 +148,10 @@ jobs:
|
||||
done
|
||||
|
||||
{
|
||||
echo "results<<'EOF'"
|
||||
echo "results<<EOF"
|
||||
cat "$RESULTS_FILE"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Ensure release labels
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
|
||||
2
.github/workflows/release-version-bump.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Manual workflow to increment package version with semantic versioning support
|
||||
name: "Release: Version Bump"
|
||||
description: "Manual workflow to increment package version with semantic versioning support"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
2
.github/workflows/weekly-docs-check.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: Automated weekly documentation accuracy check and update via Claude
|
||||
name: "Weekly Documentation Check"
|
||||
description: "Automated weekly documentation accuracy check and update via Claude"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -64,7 +64,6 @@ const config: StorybookConfig = {
|
||||
deep: true,
|
||||
extensions: ['vue']
|
||||
})
|
||||
// Note: Explicitly NOT including generateImportMapPlugin to avoid externalization
|
||||
],
|
||||
server: {
|
||||
allowedHosts: true
|
||||
|
||||
18
CODEOWNERS
@@ -1,8 +1,11 @@
|
||||
# Global Ownership
|
||||
* @Comfy-org/comfy_frontend_devs
|
||||
|
||||
# Desktop/Electron
|
||||
/apps/desktop-ui/ @webfiltered
|
||||
/src/stores/electronDownloadStore.ts @webfiltered
|
||||
/src/extensions/core/electronAdapter.ts @webfiltered
|
||||
/vite.electron.config.mts @webfiltered
|
||||
/apps/desktop-ui/ @benceruleanlu
|
||||
/src/stores/electronDownloadStore.ts @benceruleanlu
|
||||
/src/extensions/core/electronAdapter.ts @benceruleanlu
|
||||
/vite.electron.config.mts @benceruleanlu
|
||||
|
||||
# Common UI Components
|
||||
/src/components/chip/ @viva-jinyi
|
||||
@@ -31,10 +34,7 @@
|
||||
/src/components/graph/selectionToolbox/ @Myestery
|
||||
|
||||
# Minimap
|
||||
/src/renderer/extensions/minimap/ @jtydhr88
|
||||
|
||||
# Assets
|
||||
/src/platform/assets/ @arjansingh
|
||||
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
|
||||
|
||||
# Workflow Templates
|
||||
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
|
||||
@@ -53,7 +53,7 @@
|
||||
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
|
||||
|
||||
# Translations
|
||||
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer
|
||||
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs
|
||||
|
||||
# LLM Instructions (blank on purpose)
|
||||
.claude/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/desktop-ui",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"type": "module",
|
||||
"nx": {
|
||||
"tags": [
|
||||
@@ -91,7 +91,7 @@
|
||||
"build-storybook": "storybook build -o dist/storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
||||
"@comfyorg/comfyui-electron-types": "catalog:",
|
||||
"@comfyorg/shared-frontend-utils": "workspace:*",
|
||||
"@primevue/core": "catalog:",
|
||||
"@primevue/themes": "catalog:",
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="statusText" class="text-lg text-neutral-400">
|
||||
<p
|
||||
v-if="statusText"
|
||||
class="text-lg text-neutral-400"
|
||||
data-testid="startup-status-text"
|
||||
>
|
||||
{{ statusText }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -115,19 +115,18 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { type ModelRef, computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import type { ModelRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import {
|
||||
PYPI_MIRROR,
|
||||
PYTHON_MIRROR,
|
||||
type UVMirror
|
||||
} from '@/constants/uvMirrors'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR } from '@/constants/uvMirrors'
|
||||
import type { UVMirror } from '@/constants/uvMirrors'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
import MigrationPicker from './MigrationPicker.vue'
|
||||
import MirrorItem from './mirror/MirrorItem.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const installPath = defineModel<string>('installPath', { required: true })
|
||||
@@ -229,6 +228,10 @@ const validatePath = async (path: string | undefined) => {
|
||||
}
|
||||
if (validation.parentMissing) errors.push(t('install.parentMissing'))
|
||||
if (validation.isOneDrive) errors.push(t('install.isOneDrive'))
|
||||
if (validation.isInsideAppInstallDir)
|
||||
errors.push(t('install.insideAppInstallDir'))
|
||||
if (validation.isInsideUpdaterCache)
|
||||
errors.push(t('install.insideUpdaterCache'))
|
||||
|
||||
if (validation.error)
|
||||
errors.push(`${t('install.unhandledError')}: ${validation.error}`)
|
||||
|
||||
@@ -16,7 +16,8 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
||||
execute: async () => await electron.setBasePath(),
|
||||
name: 'Base path',
|
||||
shortDescription: 'Change the application base path.',
|
||||
errorDescription: 'Unable to open the base path. Please select a new one.',
|
||||
errorDescription:
|
||||
'The current base path is invalid or unsafe. Please select a new location.',
|
||||
description:
|
||||
'The base path is the default location where ComfyUI stores data. It is the location for the python environment, and may also contain models, custom nodes, and other extensions.',
|
||||
isInstallationFix: true,
|
||||
|
||||
@@ -85,6 +85,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
const electron = electronAPI()
|
||||
|
||||
// Reactive state
|
||||
const lastUpdate = ref<InstallValidation | null>(null)
|
||||
const isRefreshing = ref(false)
|
||||
const isRunningTerminalCommand = computed(() =>
|
||||
tasks.value
|
||||
@@ -97,6 +98,13 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
.some((task) => getRunner(task)?.executing)
|
||||
)
|
||||
|
||||
const unsafeBasePath = computed(
|
||||
() => lastUpdate.value?.unsafeBasePath === true
|
||||
)
|
||||
const unsafeBasePathReason = computed(
|
||||
() => lastUpdate.value?.unsafeBasePathReason
|
||||
)
|
||||
|
||||
// Task list
|
||||
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
|
||||
|
||||
@@ -123,6 +131,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
* @param validationUpdate Update details passed in by electron
|
||||
*/
|
||||
const processUpdate = (validationUpdate: InstallValidation) => {
|
||||
lastUpdate.value = validationUpdate
|
||||
const update = validationUpdate as IndexedUpdate
|
||||
isRefreshing.value = true
|
||||
|
||||
@@ -155,7 +164,11 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
}
|
||||
|
||||
const execute = async (task: MaintenanceTask) => {
|
||||
return getRunner(task).execute(task)
|
||||
const success = await getRunner(task).execute(task)
|
||||
if (success && task.isInstallationFix) {
|
||||
await refreshDesktopTasks()
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -163,6 +176,8 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
isRefreshing,
|
||||
isRunningTerminalCommand,
|
||||
isRunningInstallationFix,
|
||||
unsafeBasePath,
|
||||
unsafeBasePathReason,
|
||||
execute,
|
||||
getRunner,
|
||||
processUpdate,
|
||||
|
||||
159
apps/desktop-ui/src/views/MaintenanceView.stories.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
type UnsafeReason = 'appInstallDir' | 'updaterCache' | 'oneDrive' | null
|
||||
type ValidationIssueState = 'OK' | 'warning' | 'error' | 'skipped'
|
||||
|
||||
type ValidationState = {
|
||||
inProgress: boolean
|
||||
installState: string
|
||||
basePath?: ValidationIssueState
|
||||
unsafeBasePath: boolean
|
||||
unsafeBasePathReason: UnsafeReason
|
||||
venvDirectory?: ValidationIssueState
|
||||
pythonInterpreter?: ValidationIssueState
|
||||
pythonPackages?: ValidationIssueState
|
||||
uv?: ValidationIssueState
|
||||
git?: ValidationIssueState
|
||||
vcRedist?: ValidationIssueState
|
||||
upgradePackages?: ValidationIssueState
|
||||
}
|
||||
|
||||
const validationState: ValidationState = {
|
||||
inProgress: false,
|
||||
installState: 'installed',
|
||||
basePath: 'OK',
|
||||
unsafeBasePath: false,
|
||||
unsafeBasePathReason: null,
|
||||
venvDirectory: 'OK',
|
||||
pythonInterpreter: 'OK',
|
||||
pythonPackages: 'OK',
|
||||
uv: 'OK',
|
||||
git: 'OK',
|
||||
vcRedist: 'OK',
|
||||
upgradePackages: 'OK'
|
||||
}
|
||||
|
||||
const createMockElectronAPI = () => {
|
||||
const logListeners: Array<(message: string) => void> = []
|
||||
|
||||
const getValidationUpdate = () => ({
|
||||
...validationState
|
||||
})
|
||||
|
||||
return {
|
||||
getPlatform: () => 'darwin',
|
||||
changeTheme: (_theme: unknown) => {},
|
||||
onLogMessage: (listener: (message: string) => void) => {
|
||||
logListeners.push(listener)
|
||||
},
|
||||
showContextMenu: (_options: unknown) => {},
|
||||
Events: {
|
||||
trackEvent: (_eventName: string, _data?: unknown) => {}
|
||||
},
|
||||
Validation: {
|
||||
onUpdate: (_callback: (update: unknown) => void) => {},
|
||||
async getStatus() {
|
||||
return getValidationUpdate()
|
||||
},
|
||||
async validateInstallation(callback: (update: unknown) => void) {
|
||||
callback(getValidationUpdate())
|
||||
},
|
||||
async complete() {
|
||||
// Only allow completion when the base path is safe
|
||||
return !validationState.unsafeBasePath
|
||||
},
|
||||
dispose: () => {}
|
||||
},
|
||||
setBasePath: () => Promise.resolve(true),
|
||||
reinstall: () => Promise.resolve(),
|
||||
uv: {
|
||||
installRequirements: () => Promise.resolve(),
|
||||
clearCache: () => Promise.resolve(),
|
||||
resetVenv: () => Promise.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ensureElectronAPI = () => {
|
||||
const globalWindow = window as unknown as { electronAPI?: unknown }
|
||||
if (!globalWindow.electronAPI) {
|
||||
globalWindow.electronAPI = createMockElectronAPI()
|
||||
}
|
||||
|
||||
return globalWindow.electronAPI
|
||||
}
|
||||
|
||||
const MaintenanceView = defineAsyncComponent(async () => {
|
||||
ensureElectronAPI()
|
||||
const module = await import('./MaintenanceView.vue')
|
||||
return module.default
|
||||
})
|
||||
|
||||
const meta: Meta<typeof MaintenanceView> = {
|
||||
title: 'Desktop/Views/MaintenanceView',
|
||||
component: MaintenanceView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' },
|
||||
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
name: 'All tasks OK',
|
||||
render: () => ({
|
||||
components: { MaintenanceView },
|
||||
setup() {
|
||||
validationState.inProgress = false
|
||||
validationState.installState = 'installed'
|
||||
validationState.basePath = 'OK'
|
||||
validationState.unsafeBasePath = false
|
||||
validationState.unsafeBasePathReason = null
|
||||
validationState.venvDirectory = 'OK'
|
||||
validationState.pythonInterpreter = 'OK'
|
||||
validationState.pythonPackages = 'OK'
|
||||
validationState.uv = 'OK'
|
||||
validationState.git = 'OK'
|
||||
validationState.vcRedist = 'OK'
|
||||
validationState.upgradePackages = 'OK'
|
||||
ensureElectronAPI()
|
||||
return {}
|
||||
},
|
||||
template: '<MaintenanceView />'
|
||||
})
|
||||
}
|
||||
|
||||
export const UnsafeBasePathOneDrive: Story = {
|
||||
name: 'Unsafe base path (OneDrive)',
|
||||
render: () => ({
|
||||
components: { MaintenanceView },
|
||||
setup() {
|
||||
validationState.inProgress = false
|
||||
validationState.installState = 'installed'
|
||||
validationState.basePath = 'error'
|
||||
validationState.unsafeBasePath = true
|
||||
validationState.unsafeBasePathReason = 'oneDrive'
|
||||
validationState.venvDirectory = 'OK'
|
||||
validationState.pythonInterpreter = 'OK'
|
||||
validationState.pythonPackages = 'OK'
|
||||
validationState.uv = 'OK'
|
||||
validationState.git = 'OK'
|
||||
validationState.vcRedist = 'OK'
|
||||
validationState.upgradePackages = 'OK'
|
||||
ensureElectronAPI()
|
||||
return {}
|
||||
},
|
||||
template: '<MaintenanceView />'
|
||||
})
|
||||
}
|
||||
@@ -47,6 +47,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unsafe migration warning -->
|
||||
<div v-if="taskStore.unsafeBasePath" class="my-4">
|
||||
<p class="flex items-start gap-3 text-neutral-300">
|
||||
<Tag
|
||||
icon="pi pi-exclamation-triangle"
|
||||
severity="warn"
|
||||
:value="t('icon.exclamation-triangle')"
|
||||
/>
|
||||
<span>
|
||||
<strong class="block mb-1">
|
||||
{{ t('maintenance.unsafeMigration.title') }}
|
||||
</strong>
|
||||
<span class="block mb-1">
|
||||
{{ unsafeReasonText }}
|
||||
</span>
|
||||
<span class="block text-sm text-neutral-400">
|
||||
{{ t('maintenance.unsafeMigration.action') }}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tasks -->
|
||||
<TaskListPanel
|
||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
||||
@@ -89,10 +111,10 @@
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import Button from 'primevue/button'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import Tag from 'primevue/tag'
|
||||
import Toast from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import RefreshButton from '@/components/common/RefreshButton.vue'
|
||||
import StatusTag from '@/components/maintenance/StatusTag.vue'
|
||||
@@ -139,6 +161,27 @@ const filterOptions = ref([
|
||||
/** Filter binding; can be set to show all tasks, or only errors. */
|
||||
const filter = ref<MaintenanceFilter>(filterOptions.value[0])
|
||||
|
||||
const unsafeReasonText = computed(() => {
|
||||
const reason = taskStore.unsafeBasePathReason
|
||||
if (!reason) {
|
||||
return t('maintenance.unsafeMigration.generic')
|
||||
}
|
||||
|
||||
if (reason === 'appInstallDir') {
|
||||
return t('maintenance.unsafeMigration.appInstallDir')
|
||||
}
|
||||
|
||||
if (reason === 'updaterCache') {
|
||||
return t('maintenance.unsafeMigration.updaterCache')
|
||||
}
|
||||
|
||||
if (reason === 'oneDrive') {
|
||||
return t('maintenance.unsafeMigration.oneDrive')
|
||||
}
|
||||
|
||||
return t('maintenance.unsafeMigration.generic')
|
||||
})
|
||||
|
||||
/** If valid, leave the validation window. */
|
||||
const completeValidation = async () => {
|
||||
const isValid = await electron.Validation.complete()
|
||||
|
||||
@@ -16,7 +16,6 @@ import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
NodeLibrarySidebarTab,
|
||||
QueueSidebarTab,
|
||||
WorkflowsSidebarTab
|
||||
} from './components/SidebarTab'
|
||||
import { Topbar } from './components/Topbar'
|
||||
@@ -31,7 +30,6 @@ type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
class ComfyMenu {
|
||||
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
|
||||
private _workflowsTab: WorkflowsSidebarTab | null = null
|
||||
private _queueTab: QueueSidebarTab | null = null
|
||||
private _topbar: Topbar | null = null
|
||||
|
||||
public readonly sideToolbar: Locator
|
||||
@@ -60,11 +58,6 @@ class ComfyMenu {
|
||||
return this._workflowsTab
|
||||
}
|
||||
|
||||
get queueTab() {
|
||||
this._queueTab ??= new QueueSidebarTab(this.page)
|
||||
return this._queueTab
|
||||
}
|
||||
|
||||
get topbar() {
|
||||
this._topbar ??= new Topbar(this.page)
|
||||
return this._topbar
|
||||
@@ -564,7 +557,7 @@ export class ComfyPage {
|
||||
async dragAndDrop(source: Position, target: Position) {
|
||||
await this.page.mouse.move(source.x, source.y)
|
||||
await this.page.mouse.down()
|
||||
await this.page.mouse.move(target.x, target.y)
|
||||
await this.page.mouse.move(target.x, target.y, { steps: 100 })
|
||||
await this.page.mouse.up()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
@@ -65,7 +65,9 @@ export class VueNodeHelpers {
|
||||
* Select a specific Vue node by ID
|
||||
*/
|
||||
async selectNode(nodeId: string): Promise<void> {
|
||||
await this.page.locator(`[data-node-id="${nodeId}"]`).click()
|
||||
await this.page
|
||||
.locator(`[data-node-id="${nodeId}"] .lg-node-header`)
|
||||
.click()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,11 +79,13 @@ export class VueNodeHelpers {
|
||||
// Select first node normally
|
||||
await this.selectNode(nodeIds[0])
|
||||
|
||||
// Add additional nodes with Ctrl+click
|
||||
// Add additional nodes with Ctrl+click on header
|
||||
for (let i = 1; i < nodeIds.length; i++) {
|
||||
await this.page.locator(`[data-node-id="${nodeIds[i]}"]`).click({
|
||||
modifiers: ['Control']
|
||||
})
|
||||
await this.page
|
||||
.locator(`[data-node-id="${nodeIds[i]}"] .lg-node-header`)
|
||||
.click({
|
||||
modifiers: ['Control']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,124 +148,3 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class QueueSidebarTab extends SidebarTab {
|
||||
constructor(public readonly page: Page) {
|
||||
super(page, 'queue')
|
||||
}
|
||||
|
||||
get root() {
|
||||
return this.page.locator('.sidebar-content-container', { hasText: 'Queue' })
|
||||
}
|
||||
|
||||
get tasks() {
|
||||
return this.root.locator('[data-virtual-grid-item]')
|
||||
}
|
||||
|
||||
get visibleTasks() {
|
||||
return this.tasks.locator('visible=true')
|
||||
}
|
||||
|
||||
get clearButton() {
|
||||
return this.root.locator('.clear-all-button')
|
||||
}
|
||||
|
||||
get collapseTasksButton() {
|
||||
return this.getToggleExpandButton(false)
|
||||
}
|
||||
|
||||
get expandTasksButton() {
|
||||
return this.getToggleExpandButton(true)
|
||||
}
|
||||
|
||||
get noResultsPlaceholder() {
|
||||
return this.root.locator('.no-results-placeholder')
|
||||
}
|
||||
|
||||
get galleryImage() {
|
||||
return this.page.locator('.galleria-image')
|
||||
}
|
||||
|
||||
private getToggleExpandButton(isExpanded: boolean) {
|
||||
const iconSelector = isExpanded ? '.pi-image' : '.pi-images'
|
||||
return this.root.locator(`.toggle-expanded-button ${iconSelector}`)
|
||||
}
|
||||
|
||||
async open() {
|
||||
await super.open()
|
||||
return this.root.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async close() {
|
||||
await super.close()
|
||||
await this.root.waitFor({ state: 'hidden' })
|
||||
}
|
||||
|
||||
async expandTasks() {
|
||||
await this.expandTasksButton.click()
|
||||
await this.collapseTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async collapseTasks() {
|
||||
await this.collapseTasksButton.click()
|
||||
await this.expandTasksButton.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async waitForTasks() {
|
||||
return Promise.all([
|
||||
this.tasks.first().waitFor({ state: 'visible' }),
|
||||
this.tasks.last().waitFor({ state: 'visible' })
|
||||
])
|
||||
}
|
||||
|
||||
async scrollTasks(direction: 'up' | 'down') {
|
||||
const scrollToEl =
|
||||
direction === 'up' ? this.tasks.last() : this.tasks.first()
|
||||
await scrollToEl.scrollIntoViewIfNeeded()
|
||||
await this.waitForTasks()
|
||||
}
|
||||
|
||||
async clearTasks() {
|
||||
await this.clearButton.click()
|
||||
const confirmButton = this.page.getByLabel('Delete')
|
||||
await confirmButton.click()
|
||||
await this.noResultsPlaceholder.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
/** Set the width of the tab (out of 100). Must call before opening the tab */
|
||||
async setTabWidth(width: number) {
|
||||
if (width < 0 || width > 100) {
|
||||
throw new Error('Width must be between 0 and 100')
|
||||
}
|
||||
return this.page.evaluate((width) => {
|
||||
localStorage.setItem('queue', JSON.stringify([width, 100 - width]))
|
||||
}, width)
|
||||
}
|
||||
|
||||
getTaskPreviewButton(taskIndex: number) {
|
||||
return this.tasks.nth(taskIndex).getByRole('button')
|
||||
}
|
||||
|
||||
async openTaskPreview(taskIndex: number) {
|
||||
const previewButton = this.getTaskPreviewButton(taskIndex)
|
||||
await previewButton.click()
|
||||
return this.galleryImage.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
getGalleryImage(imageFilename: string) {
|
||||
return this.galleryImage.and(this.page.getByAltText(imageFilename))
|
||||
}
|
||||
|
||||
getTaskImage(imageFilename: string) {
|
||||
return this.tasks.getByAltText(imageFilename)
|
||||
}
|
||||
|
||||
/** Trigger the queue store and tasks to update */
|
||||
async triggerTasksUpdate() {
|
||||
await this.page.evaluate(() => {
|
||||
window['app']['api'].dispatchCustomEvent('status', {
|
||||
exec_info: { queue_remaining: 0 }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
@@ -1,210 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe.skip('Queue sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('can display tasks', async ({ comfyPage }) => {
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
|
||||
test('can display tasks after closing then opening', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.close()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
|
||||
test.describe('Virtual scroll', () => {
|
||||
const layouts = [
|
||||
{ description: 'Five columns layout', width: 95, rows: 3, cols: 5 },
|
||||
{ description: 'Three columns layout', width: 55, rows: 3, cols: 3 },
|
||||
{ description: 'Two columns layout', width: 40, rows: 3, cols: 2 }
|
||||
]
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'])
|
||||
.repeat(50)
|
||||
.setupRoutes()
|
||||
})
|
||||
|
||||
layouts.forEach(({ description, width, rows, cols }) => {
|
||||
const preRenderedRows = 1
|
||||
const preRenderedTasks = preRenderedRows * cols * 2
|
||||
const visibleTasks = rows * cols
|
||||
const expectRenderLimit = visibleTasks + preRenderedTasks
|
||||
|
||||
test.describe(description, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.setTabWidth(width)
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
})
|
||||
|
||||
test('should not render items outside of view', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
|
||||
test('should teardown items after scrolling away', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.queueTab.scrollTasks('down')
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
|
||||
test('should re-render items after scrolling away then back', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.queueTab.scrollTasks('down')
|
||||
await comfyPage.menu.queueTab.scrollTasks('up')
|
||||
const renderedCount =
|
||||
await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
expect(renderedCount).toBeLessThanOrEqual(expectRenderLimit)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Expand tasks', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// 2-item batch and 3-item batch -> 3 additional items when expanded
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp', 'example.webp', 'example.webp'])
|
||||
.withTask(['example.webp', 'example.webp'])
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
})
|
||||
|
||||
test('can expand tasks with multiple outputs', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
await comfyPage.menu.queueTab.expandTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
|
||||
initialCount + 3
|
||||
)
|
||||
})
|
||||
|
||||
test('can collapse flat tasks', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.menu.queueTab.visibleTasks.count()
|
||||
await comfyPage.menu.queueTab.expandTasks()
|
||||
await comfyPage.menu.queueTab.collapseTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(
|
||||
initialCount
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Clear tasks', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'])
|
||||
.repeat(6)
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
})
|
||||
|
||||
test('can clear all tasks', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.clearTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(0)
|
||||
expect(
|
||||
await comfyPage.menu.queueTab.noResultsPlaceholder.isVisible()
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('can load new tasks after clearing all', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.queueTab.clearTasks()
|
||||
await comfyPage.menu.queueTab.close()
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Gallery', () => {
|
||||
const firstImage = 'example.webp'
|
||||
const secondImage = 'image32x32.webp'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask([secondImage])
|
||||
.withTask([firstImage])
|
||||
.setupRoutes()
|
||||
await comfyPage.menu.queueTab.open()
|
||||
await comfyPage.menu.queueTab.waitForTasks()
|
||||
await comfyPage.menu.queueTab.openTaskPreview(0)
|
||||
})
|
||||
|
||||
test('displays gallery image after opening task preview', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintains active gallery item when new tasks are added', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Add a new task while the gallery is still open
|
||||
const newImage = 'image64x64.webp'
|
||||
comfyPage.setupHistory().withTask([newImage])
|
||||
await comfyPage.menu.queueTab.triggerTasksUpdate()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
|
||||
await newTask.waitFor({ state: 'visible' })
|
||||
// The active gallery item should still be the initial image
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Gallery navigation', () => {
|
||||
const paths: {
|
||||
description: string
|
||||
path: ('Right' | 'Left')[]
|
||||
end: string
|
||||
}[] = [
|
||||
{ description: 'Right', path: ['Right'], end: secondImage },
|
||||
{ description: 'Left', path: ['Right', 'Left'], end: firstImage },
|
||||
{ description: 'Left wrap', path: ['Left'], end: secondImage },
|
||||
{ description: 'Right wrap', path: ['Right', 'Right'], end: firstImage }
|
||||
]
|
||||
|
||||
paths.forEach(({ description, path, end }) => {
|
||||
test(`can navigate gallery ${description}`, async ({ comfyPage }) => {
|
||||
for (const direction of path)
|
||||
await comfyPage.page.keyboard.press(`Arrow${direction}`, {
|
||||
delay: 256
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(end)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
@@ -6,6 +6,7 @@ import {
|
||||
test.describe('Vue Nodes Zoom', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Node Resizing', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should resize node without position drift after selecting', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Get a Vue node fixture
|
||||
const node = await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint')
|
||||
const initialBox = await node.boundingBox()
|
||||
if (!initialBox) throw new Error('Node bounding box not found')
|
||||
|
||||
// Select the node first (this was causing the bug)
|
||||
await node.header.click()
|
||||
await comfyPage.page.waitForTimeout(100) // Brief pause after selection
|
||||
|
||||
// Get position after selection
|
||||
const selectedBox = await node.boundingBox()
|
||||
if (!selectedBox)
|
||||
throw new Error('Node bounding box not found after select')
|
||||
|
||||
// Verify position unchanged after selection
|
||||
expect(selectedBox.x).toBeCloseTo(initialBox.x, 1)
|
||||
expect(selectedBox.y).toBeCloseTo(initialBox.y, 1)
|
||||
|
||||
// Now resize from bottom-right corner
|
||||
const resizeStartX = selectedBox.x + selectedBox.width - 5
|
||||
const resizeStartY = selectedBox.y + selectedBox.height - 5
|
||||
|
||||
await comfyPage.page.mouse.move(resizeStartX, resizeStartY)
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(resizeStartX + 50, resizeStartY + 30)
|
||||
await comfyPage.page.mouse.up()
|
||||
|
||||
// Get final position and size
|
||||
const finalBox = await node.boundingBox()
|
||||
if (!finalBox) throw new Error('Node bounding box not found after resize')
|
||||
|
||||
// Position should NOT have changed (the bug was position drift)
|
||||
expect(finalBox.x).toBeCloseTo(initialBox.x, 1)
|
||||
expect(finalBox.y).toBeCloseTo(initialBox.y, 1)
|
||||
|
||||
// Size should have increased
|
||||
expect(finalBox.width).toBeGreaterThan(initialBox.width)
|
||||
expect(finalBox.height).toBeGreaterThan(initialBox.height)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 142 KiB |
@@ -1,48 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Vue Nodes - LOD', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.loadWorkflow('default')
|
||||
})
|
||||
|
||||
test('should toggle LOD based on zoom threshold', async ({ comfyPage }) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(initialNodeCount).toBeGreaterThan(0)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-default.png')
|
||||
|
||||
const vueNodesContainer = comfyPage.vueNodes.nodes
|
||||
const textboxesInNodes = vueNodesContainer.getByRole('textbox')
|
||||
const comboboxesInNodes = vueNodesContainer.getByRole('combobox')
|
||||
|
||||
await expect(textboxesInNodes.first()).toBeVisible()
|
||||
await expect(comboboxesInNodes.first()).toBeVisible()
|
||||
|
||||
await comfyPage.zoom(120, 10)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-lod-active.png')
|
||||
|
||||
await expect(textboxesInNodes.first()).toBeHidden()
|
||||
await expect(comboboxesInNodes.first()).toBeHidden()
|
||||
|
||||
await comfyPage.zoom(-120, 10)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-nodes-lod-inactive.png'
|
||||
)
|
||||
await expect(textboxesInNodes.first()).toBeVisible()
|
||||
await expect(comboboxesInNodes.first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 81 KiB |
@@ -88,12 +88,14 @@ export function comfyAPIPlugin(isDev: boolean): Plugin {
|
||||
|
||||
if (result.exports.length > 0) {
|
||||
const projectRoot = process.cwd()
|
||||
const relativePath = path.relative(path.join(projectRoot, 'src'), id)
|
||||
const relativePath = path
|
||||
.relative(path.join(projectRoot, 'src'), id)
|
||||
.replace(/\\/g, '/')
|
||||
const shimFileName = relativePath.replace(/\.ts$/, '.js')
|
||||
|
||||
let shimContent = `// Shim for ${relativePath}\n`
|
||||
|
||||
const fileKey = relativePath.replace(/\.ts$/, '').replace(/\\/g, '/')
|
||||
const fileKey = relativePath.replace(/\.ts$/, '')
|
||||
const warningMessage = getWarningMessage(fileKey, shimFileName)
|
||||
|
||||
if (warningMessage) {
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
import glob from 'fast-glob'
|
||||
import fs from 'fs-extra'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite'
|
||||
|
||||
interface ImportMapSource {
|
||||
name: string
|
||||
pattern: string | RegExp
|
||||
entry: string
|
||||
recursiveDependence?: boolean
|
||||
override?: Record<string, Partial<ImportMapSource>>
|
||||
}
|
||||
|
||||
const parseDeps = (root: string, pkg: string) => {
|
||||
const pkgPath = join(root, 'node_modules', pkg, 'package.json')
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
const content = fs.readFileSync(pkgPath, 'utf-8')
|
||||
const pkg = JSON.parse(content)
|
||||
return Object.keys(pkg.dependencies || {})
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin that generates an import map for vendor chunks.
|
||||
*
|
||||
* This plugin creates a browser-compatible import map that maps module specifiers
|
||||
* (like 'vue' or 'primevue') to their actual file locations in the build output.
|
||||
* This improves module loading in modern browsers and enables better caching.
|
||||
*
|
||||
* The plugin:
|
||||
* 1. Tracks vendor chunks during bundle generation
|
||||
* 2. Creates mappings between module names and their file paths
|
||||
* 3. Injects an import map script tag into the HTML head
|
||||
* 4. Configures manual chunk splitting for vendor libraries
|
||||
*
|
||||
* @param vendorLibraries - An array of vendor libraries to split into separate chunks
|
||||
* @returns {Plugin} A Vite plugin that generates and injects an import map
|
||||
*/
|
||||
export function generateImportMapPlugin(
|
||||
importMapSources: ImportMapSource[]
|
||||
): Plugin {
|
||||
const importMapEntries: Record<string, string> = {}
|
||||
const resolvedImportMapSources: Map<string, ImportMapSource> = new Map()
|
||||
const assetDir = 'assets/lib'
|
||||
let root: string
|
||||
|
||||
return {
|
||||
name: 'generate-import-map-plugin',
|
||||
|
||||
// Configure manual chunks during the build process
|
||||
configResolved(config) {
|
||||
root = config.root
|
||||
|
||||
if (config.build) {
|
||||
// Ensure rollupOptions exists
|
||||
if (!config.build.rollupOptions) {
|
||||
config.build.rollupOptions = {}
|
||||
}
|
||||
|
||||
for (const source of importMapSources) {
|
||||
resolvedImportMapSources.set(source.name, source)
|
||||
if (source.recursiveDependence) {
|
||||
const deps = parseDeps(root, source.name)
|
||||
|
||||
while (deps.length) {
|
||||
const dep = deps.shift()!
|
||||
const depSource = Object.assign({}, source, {
|
||||
name: dep,
|
||||
pattern: dep,
|
||||
...source.override?.[dep]
|
||||
})
|
||||
resolvedImportMapSources.set(depSource.name, depSource)
|
||||
|
||||
const _deps = parseDeps(root, depSource.name)
|
||||
deps.unshift(..._deps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const external: (string | RegExp)[] = []
|
||||
for (const [, source] of resolvedImportMapSources) {
|
||||
external.push(source.pattern)
|
||||
}
|
||||
config.build.rollupOptions.external = external
|
||||
}
|
||||
},
|
||||
|
||||
generateBundle(_options) {
|
||||
for (const [, source] of resolvedImportMapSources) {
|
||||
if (source.entry) {
|
||||
const moduleFile = join(source.name, source.entry)
|
||||
const sourceFile = join(root, 'node_modules', moduleFile)
|
||||
const targetFile = join(root, 'dist', assetDir, moduleFile)
|
||||
|
||||
importMapEntries[source.name] =
|
||||
'./' + normalizePath(join(assetDir, moduleFile))
|
||||
|
||||
const targetDir = dirname(targetFile)
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true })
|
||||
}
|
||||
fs.copyFileSync(sourceFile, targetFile)
|
||||
}
|
||||
|
||||
if (source.recursiveDependence) {
|
||||
const files = glob.sync(['**/*.{js,mjs}'], {
|
||||
cwd: join(root, 'node_modules', source.name)
|
||||
})
|
||||
|
||||
for (const file of files) {
|
||||
const moduleFile = join(source.name, file)
|
||||
const sourceFile = join(root, 'node_modules', moduleFile)
|
||||
const targetFile = join(root, 'dist', assetDir, moduleFile)
|
||||
|
||||
importMapEntries[normalizePath(join(source.name, dirname(file)))] =
|
||||
'./' + normalizePath(join(assetDir, moduleFile))
|
||||
|
||||
const targetDir = dirname(targetFile)
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true })
|
||||
}
|
||||
fs.copyFileSync(sourceFile, targetFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
transformIndexHtml(html) {
|
||||
if (Object.keys(importMapEntries).length === 0) {
|
||||
console.warn(
|
||||
'[ImportMap Plugin] No vendor chunks found to create import map.'
|
||||
)
|
||||
return html
|
||||
}
|
||||
|
||||
const importMap = {
|
||||
imports: importMapEntries
|
||||
}
|
||||
|
||||
const importMapTag: HtmlTagDescriptor = {
|
||||
tag: 'script',
|
||||
attrs: { type: 'importmap' },
|
||||
children: JSON.stringify(importMap, null, 2),
|
||||
injectTo: 'head'
|
||||
}
|
||||
|
||||
return {
|
||||
html,
|
||||
tags: [importMapTag]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
export { comfyAPIPlugin } from './comfyAPIPlugin'
|
||||
export { generateImportMapPlugin } from './generateImportMapPlugin'
|
||||
|
||||
24
cloud-loader-dropdown.md
Normal file
@@ -0,0 +1,24 @@
|
||||
Fixes loader dropdown placeholder
|
||||
===============================
|
||||
|
||||
Cloud loader dropdowns hydrate via `useAssetWidgetData(nodeType)`, so `dropdownItems` stays empty until the Asset API returns friendly filenames. Meanwhile `modelValue` already holds the saved asset and the watcher at [WidgetSelectDropdown.vue#L215-L227](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue#L215-L227) only tracks `modelValue`. It runs before assets load, fails to find a match, clears `selectedSet`, and the placeholder persists.
|
||||
|
||||
```ts
|
||||
watch(
|
||||
modelValue,
|
||||
(currentValue) => {
|
||||
if (currentValue === undefined) {
|
||||
selectedSet.value.clear()
|
||||
return
|
||||
}
|
||||
const item = dropdownItems.value.find((item) => item.name === currentValue)
|
||||
if (item) {
|
||||
selectedSet.value.clear()
|
||||
selectedSet.value.add(item.id)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
```
|
||||
|
||||
Once the API resolves, `dropdownItems` recomputes but nothing resyncs because the watcher never sees that change. Desktop doesn’t hit this because it still reads from `widget.options.values` immediately.
|
||||
@@ -191,6 +191,19 @@ export default defineConfig([
|
||||
'@intlify/vue-i18n/no-raw-text': [
|
||||
'error',
|
||||
{
|
||||
attributes: {
|
||||
'/.+/': [
|
||||
'aria-label',
|
||||
'aria-placeholder',
|
||||
'aria-roledescription',
|
||||
'aria-valuetext',
|
||||
'label',
|
||||
'placeholder',
|
||||
'title',
|
||||
'v-tooltip'
|
||||
],
|
||||
img: ['alt']
|
||||
},
|
||||
// Ignore strings that are:
|
||||
// 1. Less than 2 characters
|
||||
// 2. Only symbols/numbers/whitespace (no letters)
|
||||
@@ -200,24 +213,27 @@ export default defineConfig([
|
||||
ignoreNodes: ['md-icon', 'v-icon', 'pre', 'code', 'script', 'style'],
|
||||
// Brand names and technical terms that shouldn't be translated
|
||||
ignoreText: [
|
||||
'ComfyUI',
|
||||
'GitHub',
|
||||
'OpenAI',
|
||||
'API',
|
||||
'URL',
|
||||
'JSON',
|
||||
'YAML',
|
||||
'GPU',
|
||||
'CPU',
|
||||
'RAM',
|
||||
'GB',
|
||||
'MB',
|
||||
'KB',
|
||||
'ms',
|
||||
'fps',
|
||||
'px',
|
||||
'App Data:',
|
||||
'App Path:'
|
||||
'App Path:',
|
||||
'ComfyUI',
|
||||
'CPU',
|
||||
'fps',
|
||||
'GB',
|
||||
'GitHub',
|
||||
'GPU',
|
||||
'JSON',
|
||||
'KB',
|
||||
'LoRA',
|
||||
'MB',
|
||||
'ms',
|
||||
'OpenAI',
|
||||
'png',
|
||||
'px',
|
||||
'RAM',
|
||||
'URL',
|
||||
'YAML',
|
||||
'1.2 MB'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.32.6",
|
||||
"version": "1.33.9",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -75,6 +75,7 @@
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"@vitest/ui": "catalog:",
|
||||
"@vue/test-utils": "catalog:",
|
||||
"@webgpu/types": "catalog:",
|
||||
"cross-env": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
@@ -112,6 +113,7 @@
|
||||
"typescript": "catalog:",
|
||||
"typescript-eslint": "catalog:",
|
||||
"unplugin-icons": "catalog:",
|
||||
"unplugin-typegpu": "catalog:",
|
||||
"unplugin-vue-components": "catalog:",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "catalog:",
|
||||
@@ -128,7 +130,7 @@
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "catalog:",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
||||
"@comfyorg/comfyui-electron-types": "catalog:",
|
||||
"@comfyorg/design-system": "workspace:*",
|
||||
"@comfyorg/registry-types": "workspace:*",
|
||||
"@comfyorg/shared-frontend-utils": "workspace:*",
|
||||
@@ -162,7 +164,6 @@
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"fast-glob": "^3.3.3",
|
||||
"firebase": "catalog:",
|
||||
"fuse.js": "^7.0.0",
|
||||
"glob": "^11.0.3",
|
||||
@@ -176,6 +177,7 @@
|
||||
"semver": "^7.7.2",
|
||||
"three": "^0.170.0",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"typegpu": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-i18n": "catalog:",
|
||||
"vue-router": "catalog:",
|
||||
|
||||
@@ -1329,57 +1329,6 @@ audio.comfy-audio.empty-audio-widget {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* START LOD specific styles */
|
||||
/* LOD styles - Custom CSS avoids 100+ Tailwind selectors that would slow style recalculation when .isLOD toggles */
|
||||
|
||||
.isLOD .lg-node {
|
||||
box-shadow: none;
|
||||
filter: none;
|
||||
backdrop-filter: none;
|
||||
text-shadow: none;
|
||||
mask-image: none;
|
||||
clip-path: none;
|
||||
background-image: none;
|
||||
text-rendering: optimizeSpeed;
|
||||
border-radius: 0;
|
||||
contain: layout style;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.isLOD .lg-node-header {
|
||||
border-radius: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.isLOD .lg-node-widgets {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lod-toggle {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.isLOD .lod-toggle {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.lod-fallback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.isLOD .lod-fallback {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.isLOD .image-preview img {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.isLOD .slot-dot {
|
||||
border-radius: 0;
|
||||
}
|
||||
/* END LOD specific styles */
|
||||
|
||||
/* ===================== Mask Editor Styles ===================== */
|
||||
/* To be migrated to Tailwind later */
|
||||
#maskEditor_brush {
|
||||
|
||||
@@ -75,6 +75,17 @@ export function formatSize(value?: number) {
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a commit hash by truncating long (40-char) hashes to 7 chars.
|
||||
* Returns the original string if not a valid full commit hash.
|
||||
*/
|
||||
export function formatCommitHash(value: string): string {
|
||||
if (/^[a-f0-9]{40}$/i.test(value)) {
|
||||
return value.slice(0, 7)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns various filename components.
|
||||
* Example:
|
||||
|
||||
150
pnpm-lock.yaml
generated
@@ -9,18 +9,15 @@ catalogs:
|
||||
'@alloc/quick-lru':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
'@comfyorg/comfyui-electron-types':
|
||||
specifier: 0.5.5
|
||||
version: 0.5.5
|
||||
'@eslint/js':
|
||||
specifier: ^9.35.0
|
||||
version: 9.35.0
|
||||
'@iconify-json/lucide':
|
||||
specifier: ^1.1.178
|
||||
version: 1.2.66
|
||||
'@iconify/json':
|
||||
specifier: ^2.2.380
|
||||
version: 2.2.380
|
||||
'@iconify/tailwind':
|
||||
specifier: ^1.1.3
|
||||
version: 1.2.0
|
||||
'@intlify/eslint-plugin-vue-i18n':
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
@@ -123,6 +120,9 @@ catalogs:
|
||||
'@vueuse/integrations':
|
||||
specifier: ^13.9.0
|
||||
version: 13.9.0
|
||||
'@webgpu/types':
|
||||
specifier: ^0.1.66
|
||||
version: 0.1.66
|
||||
algoliasearch:
|
||||
specifier: ^5.21.0
|
||||
version: 5.21.0
|
||||
@@ -243,6 +243,9 @@ catalogs:
|
||||
tw-animate-css:
|
||||
specifier: ^1.3.8
|
||||
version: 1.3.8
|
||||
typegpu:
|
||||
specifier: ^0.8.2
|
||||
version: 0.8.2
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
@@ -252,6 +255,9 @@ catalogs:
|
||||
unplugin-icons:
|
||||
specifier: ^0.22.0
|
||||
version: 0.22.0
|
||||
unplugin-typegpu:
|
||||
specifier: 0.8.0
|
||||
version: 0.8.0
|
||||
unplugin-vue-components:
|
||||
specifier: ^0.28.0
|
||||
version: 0.28.0
|
||||
@@ -318,8 +324,8 @@ importers:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
'@comfyorg/comfyui-electron-types':
|
||||
specifier: 0.4.73-0
|
||||
version: 0.4.73-0
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.5
|
||||
'@comfyorg/design-system':
|
||||
specifier: workspace:*
|
||||
version: link:packages/design-system
|
||||
@@ -419,9 +425,6 @@ importers:
|
||||
extendable-media-recorder-wav-encoder:
|
||||
specifier: ^7.0.129
|
||||
version: 7.0.129
|
||||
fast-glob:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
firebase:
|
||||
specifier: 'catalog:'
|
||||
version: 11.6.0
|
||||
@@ -461,6 +464,9 @@ importers:
|
||||
tiptap-markdown:
|
||||
specifier: ^0.8.10
|
||||
version: 0.8.10(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))
|
||||
typegpu:
|
||||
specifier: 'catalog:'
|
||||
version: 0.8.2
|
||||
vue:
|
||||
specifier: 'catalog:'
|
||||
version: 3.5.13(typescript@5.9.2)
|
||||
@@ -558,6 +564,9 @@ importers:
|
||||
'@vue/test-utils':
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.6
|
||||
'@webgpu/types':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.66
|
||||
cross-env:
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.0
|
||||
@@ -669,6 +678,9 @@ importers:
|
||||
unplugin-icons:
|
||||
specifier: 'catalog:'
|
||||
version: 0.22.0(@vue/compiler-sfc@3.5.13)
|
||||
unplugin-typegpu:
|
||||
specifier: 'catalog:'
|
||||
version: 0.8.0(typegpu@0.8.2)
|
||||
unplugin-vue-components:
|
||||
specifier: 'catalog:'
|
||||
version: 0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2))
|
||||
@@ -709,8 +721,8 @@ importers:
|
||||
apps/desktop-ui:
|
||||
dependencies:
|
||||
'@comfyorg/comfyui-electron-types':
|
||||
specifier: 0.4.73-0
|
||||
version: 0.4.73-0
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.5
|
||||
'@comfyorg/shared-frontend-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared-frontend-utils
|
||||
@@ -1428,6 +1440,10 @@ packages:
|
||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/standalone@7.28.5':
|
||||
resolution: {integrity: sha512-1DViPYJpRU50irpGMfLBQ9B4kyfQuL6X7SS7pwTeWeZX0mNkjzPi0XFqxCjSdddZXUQy4AhnQnnesA/ZHnvAdw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1453,8 +1469,8 @@ packages:
|
||||
'@cacheable/utils@2.0.3':
|
||||
resolution: {integrity: sha512-m7Rce68cMHlAUjvWBy9Ru1Nmw5gU0SjGGtQDdhpe6E0xnbcvrIY0Epy//JU1VYYBUTzrG9jvgmTauULGKzOkWA==}
|
||||
|
||||
'@comfyorg/comfyui-electron-types@0.4.73-0':
|
||||
resolution: {integrity: sha512-WlItGJQx9ZWShNG9wypx3kq+19pSig/U+s5sD2SAeEcMph4u8A/TS+lnRgdKhT58VT1uD7cMcj2SJpfdBPNWvw==}
|
||||
'@comfyorg/comfyui-electron-types@0.5.5':
|
||||
resolution: {integrity: sha512-f3XOXpMsALIwHakz7FekVPm4/Fh2pvJPEi8tRe8jYGBt8edsd4Mkkq31Yjs2Weem3BP7yNwbdNuSiQdP/pxJyg==}
|
||||
|
||||
'@csstools/color-helpers@5.1.0':
|
||||
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
||||
@@ -3787,8 +3803,8 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@webgpu/types@0.1.51':
|
||||
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==}
|
||||
'@webgpu/types@0.1.66':
|
||||
resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==}
|
||||
|
||||
'@xstate/fsm@1.6.5':
|
||||
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
|
||||
@@ -4413,6 +4429,9 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -6032,6 +6051,10 @@ packages:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
|
||||
magic-string-ast@1.0.3:
|
||||
resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
magic-string@0.30.19:
|
||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||
|
||||
@@ -7000,6 +7023,11 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
hasBin: true
|
||||
|
||||
resolve@1.22.11:
|
||||
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
hasBin: true
|
||||
|
||||
restore-cursor@3.1.0:
|
||||
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7095,6 +7123,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
set-function-length@1.2.2:
|
||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -7395,6 +7428,14 @@ packages:
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
tinyest-for-wgsl@0.1.3:
|
||||
resolution: {integrity: sha512-Wm5ADG1UyDxykf42S1gLYP4U9e1QP/TdtJeovQi6y68zttpiFLKqQGioHmPs9Mjysh7YMSAr/Lpuk0cD2MVdGA==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
tinyest@0.1.2:
|
||||
resolution: {integrity: sha512-aHRmouyowIq1P5jrTF+YK6pGX+WuvFtSCLbqk91yHnU3SWQRIcNIamZLM5XF6lLqB13AWz0PGPXRff2QGDsxIg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
@@ -7521,6 +7562,13 @@ packages:
|
||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
typed-binary@4.3.2:
|
||||
resolution: {integrity: sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ==}
|
||||
|
||||
typegpu@0.8.2:
|
||||
resolution: {integrity: sha512-wkMJWhJE0pSkw2G/FesjqjbtHkREyOKu1Zmyj19xfmaX5+65YFwgfQNKSK8CxqN4kJkP7JFelLDJTSYY536TYg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
typescript-eslint@8.44.0:
|
||||
resolution: {integrity: sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -7625,6 +7673,11 @@ packages:
|
||||
vue-template-es2015-compiler:
|
||||
optional: true
|
||||
|
||||
unplugin-typegpu@0.8.0:
|
||||
resolution: {integrity: sha512-VJHdXSXGOkAx0WhwFczhVUjAI6HyDkrQXk20HnwyuzIE3FdqE5l9sJTCYZzoVGo3z8i/IA5TMHCDzzP0Bc97Cw==}
|
||||
peerDependencies:
|
||||
typegpu: ^0.8.0
|
||||
|
||||
unplugin-vue-components@0.28.0:
|
||||
resolution: {integrity: sha512-jiTGtJ3JsRFBjgvyilfrX7yUoGKScFgbdNw+6p6kEXU+Spf/rhxzgvdfuMcvhCcLmflB/dY3pGQshYBVGOUx7Q==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -7815,8 +7868,8 @@ packages:
|
||||
vue-component-type-helpers@3.1.1:
|
||||
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
|
||||
|
||||
vue-component-type-helpers@3.1.3:
|
||||
resolution: {integrity: sha512-V1dOD8XYfstOKCnXbWyEJIrhTBMwSyNjv271L1Jlx9ExpNlCSuqOs3OdWrGJ0V544zXufKbcYabi/o+gK8lyfQ==}
|
||||
vue-component-type-helpers@3.1.5:
|
||||
resolution: {integrity: sha512-7V3yJuNWW7/1jxCcI1CswnpDsvs02Qcx/N43LkV+ZqhLj2PKj50slUflHAroNkN4UWiYfzMUUUXiNuv9khmSpQ==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
@@ -8953,6 +9006,8 @@ snapshots:
|
||||
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
||||
'@babel/standalone@7.28.5': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
@@ -8992,7 +9047,7 @@ snapshots:
|
||||
|
||||
'@cacheable/utils@2.0.3': {}
|
||||
|
||||
'@comfyorg/comfyui-electron-types@0.4.73-0': {}
|
||||
'@comfyorg/comfyui-electron-types@0.5.5': {}
|
||||
|
||||
'@csstools/color-helpers@5.1.0': {}
|
||||
|
||||
@@ -10617,7 +10672,7 @@ snapshots:
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.1.3
|
||||
vue-component-type-helpers: 3.1.5
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
dependencies:
|
||||
@@ -10989,7 +11044,7 @@ snapshots:
|
||||
|
||||
'@types/react@19.1.9':
|
||||
dependencies:
|
||||
csstype: 3.1.3
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/semver@7.7.0': {}
|
||||
|
||||
@@ -11000,7 +11055,7 @@ snapshots:
|
||||
'@tweenjs/tween.js': 23.1.3
|
||||
'@types/stats.js': 0.17.3
|
||||
'@types/webxr': 0.5.20
|
||||
'@webgpu/types': 0.1.51
|
||||
'@webgpu/types': 0.1.66
|
||||
fflate: 0.8.2
|
||||
meshoptimizer: 0.18.1
|
||||
|
||||
@@ -11503,7 +11558,7 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
|
||||
'@webgpu/types@0.1.51': {}
|
||||
'@webgpu/types@0.1.66': {}
|
||||
|
||||
'@xstate/fsm@1.6.5': {}
|
||||
|
||||
@@ -12168,6 +12223,8 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
dependencies:
|
||||
whatwg-mimetype: 4.0.0
|
||||
@@ -12594,7 +12651,7 @@ snapshots:
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
is-core-module: 2.16.1
|
||||
resolve: 1.22.10
|
||||
resolve: 1.22.11
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
@@ -13740,7 +13797,7 @@ snapshots:
|
||||
acorn: 8.15.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
jsonc-parser@3.2.0: {}
|
||||
|
||||
@@ -13982,6 +14039,10 @@ snapshots:
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
magic-string-ast@1.0.3:
|
||||
dependencies:
|
||||
magic-string: 0.30.19
|
||||
|
||||
magic-string@0.30.19:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -15345,6 +15406,13 @@ snapshots:
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
resolve@1.22.11:
|
||||
dependencies:
|
||||
is-core-module: 2.16.1
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
optional: true
|
||||
|
||||
restore-cursor@3.1.0:
|
||||
dependencies:
|
||||
onetime: 5.1.2
|
||||
@@ -15449,6 +15517,8 @@ snapshots:
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
semver@7.7.3: {}
|
||||
|
||||
set-function-length@1.2.2:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
@@ -15837,6 +15907,12 @@ snapshots:
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyest-for-wgsl@0.1.3:
|
||||
dependencies:
|
||||
tinyest: 0.1.2
|
||||
|
||||
tinyest@0.1.2: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinyexec@1.0.1: {}
|
||||
@@ -15968,6 +16044,13 @@ snapshots:
|
||||
reflect.getprototypeof: 1.0.10
|
||||
optional: true
|
||||
|
||||
typed-binary@4.3.2: {}
|
||||
|
||||
typegpu@0.8.2:
|
||||
dependencies:
|
||||
tinyest: 0.1.2
|
||||
typed-binary: 4.3.2
|
||||
|
||||
typescript-eslint@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)
|
||||
@@ -16063,6 +16146,19 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-typegpu@0.8.0(typegpu@0.8.2):
|
||||
dependencies:
|
||||
'@babel/standalone': 7.28.5
|
||||
defu: 6.1.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string-ast: 1.0.3
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
tinyest: 0.1.2
|
||||
tinyest-for-wgsl: 0.1.3
|
||||
typegpu: 0.8.2
|
||||
unplugin: 2.3.5
|
||||
|
||||
unplugin-vue-components@0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.10
|
||||
@@ -16343,7 +16439,7 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@3.1.1: {}
|
||||
|
||||
vue-component-type-helpers@3.1.3: {}
|
||||
vue-component-type-helpers@3.1.5: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
|
||||
@@ -4,6 +4,7 @@ packages:
|
||||
|
||||
catalog:
|
||||
'@alloc/quick-lru': ^5.2.0
|
||||
'@comfyorg/comfyui-electron-types': 0.5.5
|
||||
'@eslint/js': ^9.35.0
|
||||
'@iconify-json/lucide': ^1.1.178
|
||||
'@iconify/json': ^2.2.380
|
||||
@@ -42,6 +43,7 @@ catalog:
|
||||
'@vue/test-utils': ^2.4.6
|
||||
'@vueuse/core': ^11.0.0
|
||||
'@vueuse/integrations': ^13.9.0
|
||||
'@webgpu/types': ^0.1.66
|
||||
algoliasearch: ^5.21.0
|
||||
axios: ^1.8.2
|
||||
cross-env: ^10.1.0
|
||||
@@ -82,9 +84,11 @@ catalog:
|
||||
tailwindcss-primeui: ^0.6.1
|
||||
tsx: ^4.15.6
|
||||
tw-animate-css: ^1.3.8
|
||||
typegpu: ^0.8.2
|
||||
typescript: ^5.9.2
|
||||
typescript-eslint: ^8.44.0
|
||||
unplugin-icons: ^0.22.0
|
||||
unplugin-typegpu: 0.8.0
|
||||
unplugin-vue-components: ^0.28.0
|
||||
vite: ^5.4.19
|
||||
vite-plugin-dts: ^4.5.4
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
Thanks to OpenArt (https://openart.ai) for providing the sorted-custom-node-map data, captured in September 2024.
|
||||
Node usage data merged from two sources:
|
||||
- Mixpanel "app:node_search_result_selected" events (Nov 2025): 1,112 nodes, 46,514 selections
|
||||
Reflects actual user search behavior - what users choose when searching.
|
||||
- OpenArt workflow data (Sept 2024): 2,600 nodes, 118,676 uses
|
||||
Reflects overall popularity - what's used in workflows.
|
||||
|
||||
Merge strategy: New data overwrites old for 514 overlapping nodes. Old data
|
||||
normalized by 2.55x scale factor to match new data. Total: 3,198 nodes.
|
||||
Search-selected nodes prioritized in ranking.
|
||||
9
public/assets/images/civitai.svg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-1 flex gap-x-0.5 pt-1">
|
||||
<div
|
||||
v-if="!workspaceStore.focusMode"
|
||||
class="ml-1 flex gap-x-0.5 pt-1"
|
||||
@mouseenter="isTopMenuHovered = true"
|
||||
@mouseleave="isTopMenuHovered = false"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
</div>
|
||||
@@ -40,7 +45,10 @@
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
</div>
|
||||
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
|
||||
<QueueProgressOverlay
|
||||
v-model:expanded="isQueueOverlayExpanded"
|
||||
:menu-hovered="isTopMenuHovered"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -69,6 +77,7 @@ const isDesktop = isElectron()
|
||||
const { t } = useI18n()
|
||||
const isQueueOverlayExpanded = ref(false)
|
||||
const queueStore = useQueueStore()
|
||||
const isTopMenuHovered = ref(false)
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||
|
||||
@@ -257,7 +257,7 @@ watch(isDragging, (dragging) => {
|
||||
})
|
||||
const actionbarClass = computed(() =>
|
||||
cn(
|
||||
'w-[265px] border-dashed border-blue-500 opacity-80',
|
||||
'w-[200px] border-dashed border-blue-500 opacity-80',
|
||||
'm-1.5 flex items-center justify-center self-stretch',
|
||||
'rounded-md before:w-50 before:-ml-50 before:h-full',
|
||||
'pointer-events-auto',
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
severity="primary"
|
||||
size="small"
|
||||
:model="queueModeMenuItems"
|
||||
:disabled="hasMissingNodes"
|
||||
data-testid="queue-button"
|
||||
@click="queuePrompt"
|
||||
>
|
||||
@@ -32,46 +31,12 @@
|
||||
</template>
|
||||
</SplitButton>
|
||||
<BatchCountEdit />
|
||||
<ButtonGroup class="execution-actions flex flex-nowrap">
|
||||
<Button
|
||||
v-tooltip.bottom="{
|
||||
value: $t('menu.interrupt'),
|
||||
showDelay: 600
|
||||
}"
|
||||
icon="pi pi-times"
|
||||
:severity="executingPrompt ? 'danger' : 'secondary'"
|
||||
:disabled="!executingPrompt"
|
||||
text
|
||||
:aria-label="$t('menu.interrupt')"
|
||||
@click="() => commandStore.execute('Comfy.Interrupt')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="{
|
||||
value: $t('sideToolbar.queueTab.clearPendingTasks'),
|
||||
showDelay: 600
|
||||
}"
|
||||
icon="pi pi-stop"
|
||||
:severity="hasPendingTasks ? 'danger' : 'secondary'"
|
||||
:disabled="!hasPendingTasks"
|
||||
text
|
||||
:aria-label="$t('sideToolbar.queueTab.clearPendingTasks')"
|
||||
@click="
|
||||
() => {
|
||||
if (queueCountStore.count.value > 1) {
|
||||
commandStore.execute('Comfy.ClearPendingTasks')
|
||||
}
|
||||
queueMode = 'disabled'
|
||||
}
|
||||
"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import { computed } from 'vue'
|
||||
@@ -79,21 +44,22 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
useQueuePendingTaskCountStore,
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
|
||||
|
||||
import BatchCountEdit from '../BatchCountEdit.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const { hasMissingNodes } = useMissingNodes()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const hasMissingNodes = computed(() =>
|
||||
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
const queueModeMenuItemLookup = computed(() => {
|
||||
@@ -145,11 +111,6 @@ const queueModeMenuItems = computed(() =>
|
||||
Object.values(queueModeMenuItemLookup.value)
|
||||
)
|
||||
|
||||
const executingPrompt = computed(() => !!queueCountStore.count.value)
|
||||
const hasPendingTasks = computed(
|
||||
() => queueCountStore.count.value > 1 || queueMode.value !== 'disabled'
|
||||
)
|
||||
|
||||
const iconClass = computed(() => {
|
||||
if (hasMissingNodes.value) {
|
||||
return 'icon-[lucide--triangle-alert]'
|
||||
|
||||
@@ -64,11 +64,13 @@ import {
|
||||
ComfyWorkflow,
|
||||
useWorkflowStore
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
|
||||
|
||||
interface Props {
|
||||
item: MenuItem
|
||||
@@ -79,7 +81,10 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
isActive: false
|
||||
})
|
||||
|
||||
const { hasMissingNodes } = useMissingNodes()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const hasMissingNodes = computed(() =>
|
||||
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
const menu = ref<InstanceType<typeof Menu> & MenuState>()
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface IconButtonProps extends BaseButtonProps {
|
||||
onClick: (event: Event) => void
|
||||
onClick?: (event: MouseEvent) => void
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -47,7 +47,7 @@ const {
|
||||
} = defineProps<IconTextButtonProps>()
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} justify-start! gap-2`
|
||||
const baseClasses = `${getBaseButtonClasses()} justify-start gap-2`
|
||||
const sizeClasses = getButtonSizeClasses(size)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
|
||||
@@ -68,4 +68,8 @@ const toggle = (event: Event) => {
|
||||
const hide = () => {
|
||||
popover.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
hide
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -9,29 +9,31 @@
|
||||
<div class="font-medium">
|
||||
{{ col.header }}
|
||||
</div>
|
||||
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
|
||||
<div>{{ getDisplayValue(col) }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<template v-if="hasDevices">
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl font-semibold">
|
||||
{{ $t('g.devices') }}
|
||||
</h2>
|
||||
<TabView v-if="props.stats.devices.length > 1">
|
||||
<TabPanel
|
||||
v-for="device in props.stats.devices"
|
||||
:key="device.index"
|
||||
:header="device.name"
|
||||
:value="device.index"
|
||||
>
|
||||
<DeviceInfo :device="device" />
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
<DeviceInfo v-else :device="props.stats.devices[0]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl font-semibold">
|
||||
{{ $t('g.devices') }}
|
||||
</h2>
|
||||
<TabView v-if="props.stats.devices.length > 1">
|
||||
<TabPanel
|
||||
v-for="device in props.stats.devices"
|
||||
:key="device.index"
|
||||
:header="device.name"
|
||||
:value="device.index"
|
||||
>
|
||||
<DeviceInfo :device="device" />
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
<DeviceInfo v-else :device="props.stats.devices[0]" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -42,8 +44,9 @@ import TabView from 'primevue/tabview'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import DeviceInfo from '@/components/common/DeviceInfo.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
import { formatCommitHash, formatSize } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
stats: SystemStats
|
||||
@@ -54,20 +57,53 @@ const systemInfo = computed(() => ({
|
||||
argv: props.stats.system.argv.join(' ')
|
||||
}))
|
||||
|
||||
const systemColumns: { field: keyof SystemStats['system']; header: string }[] =
|
||||
[
|
||||
{ field: 'os', header: 'OS' },
|
||||
{ field: 'python_version', header: 'Python Version' },
|
||||
{ field: 'embedded_python', header: 'Embedded Python' },
|
||||
{ field: 'pytorch_version', header: 'Pytorch Version' },
|
||||
{ field: 'argv', header: 'Arguments' },
|
||||
{ field: 'ram_total', header: 'RAM Total' },
|
||||
{ field: 'ram_free', header: 'RAM Free' }
|
||||
]
|
||||
const hasDevices = computed(() => props.stats.devices.length > 0)
|
||||
|
||||
const formatValue = (value: any, field: string) => {
|
||||
if (['ram_total', 'ram_free'].includes(field)) {
|
||||
return formatSize(value)
|
||||
type SystemInfoKey = keyof SystemStats['system']
|
||||
|
||||
type ColumnDef = {
|
||||
field: SystemInfoKey
|
||||
header: string
|
||||
format?: (value: string) => string
|
||||
formatNumber?: (value: number) => string
|
||||
}
|
||||
|
||||
/** Columns for local distribution */
|
||||
const localColumns: ColumnDef[] = [
|
||||
{ field: 'os', header: 'OS' },
|
||||
{ field: 'python_version', header: 'Python Version' },
|
||||
{ field: 'embedded_python', header: 'Embedded Python' },
|
||||
{ field: 'pytorch_version', header: 'Pytorch Version' },
|
||||
{ field: 'argv', header: 'Arguments' },
|
||||
{ field: 'ram_total', header: 'RAM Total', formatNumber: formatSize },
|
||||
{ field: 'ram_free', header: 'RAM Free', formatNumber: formatSize }
|
||||
]
|
||||
|
||||
/** Columns for cloud distribution */
|
||||
const cloudColumns: ColumnDef[] = [
|
||||
{ field: 'cloud_version', header: 'Cloud Version' },
|
||||
{
|
||||
field: 'comfyui_version',
|
||||
header: 'ComfyUI Version',
|
||||
format: formatCommitHash
|
||||
},
|
||||
{
|
||||
field: 'comfyui_frontend_version',
|
||||
header: 'Frontend Version',
|
||||
format: formatCommitHash
|
||||
},
|
||||
{ field: 'workflow_templates_version', header: 'Templates Version' }
|
||||
]
|
||||
|
||||
const systemColumns = computed(() => (isCloud ? cloudColumns : localColumns))
|
||||
|
||||
const getDisplayValue = (column: ColumnDef) => {
|
||||
const value = systemInfo.value[column.field]
|
||||
if (column.formatNumber && typeof value === 'number') {
|
||||
return column.formatNumber(value)
|
||||
}
|
||||
if (column.format && typeof value === 'string') {
|
||||
return column.format(value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
class="w-62.5"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--arrow-up-down]" />
|
||||
<i class="icon-[lucide--arrow-up-down] text-muted-foreground" />
|
||||
</template>
|
||||
</SingleSelect>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex w-[490px] flex-col border-t-1 border-b-1 border-border-default"
|
||||
class="flex w-[490px] flex-col border-t-1 border-border-default"
|
||||
:class="isCloud ? 'border-b-1' : ''"
|
||||
>
|
||||
<div class="flex h-full w-full flex-col gap-4 p-4">
|
||||
<!-- Description -->
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<InputText
|
||||
ref="inputRef"
|
||||
v-model="inputValue"
|
||||
:placeholder
|
||||
autofocus
|
||||
@keyup.enter="onConfirm"
|
||||
@focus="selectAllText"
|
||||
@@ -28,6 +29,7 @@ const props = defineProps<{
|
||||
message: string
|
||||
defaultValue: string
|
||||
onConfirm: (value: string) => void
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const inputValue = ref<string>(props.defaultValue)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
|
||||
class: cn(
|
||||
'h-10 relative inline-flex cursor-pointer select-none',
|
||||
'rounded-lg bg-base-background text-base-foreground',
|
||||
'rounded-lg bg-secondary-background text-base-foreground',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'border-[2.5px] border-solid',
|
||||
selectedCount > 0
|
||||
@@ -83,7 +83,7 @@
|
||||
role="combobox"
|
||||
:aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
tabindex="0"
|
||||
:tabindex="0"
|
||||
>
|
||||
<template
|
||||
v-if="showSearchBox || showSelectedCount || showClearButton"
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
<!-- Trigger value (keep text scale identical) -->
|
||||
<template #value>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
<span class="text-sm">
|
||||
{{ label }}
|
||||
</span>
|
||||
<span
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
<!-- Chevron size identical to current -->
|
||||
<template #dropdownicon>
|
||||
<i class="icon-[lucide--chevron-down] text-lg text-neutral-400" />
|
||||
<i class="icon-[lucide--chevron-down] text-muted-foreground" />
|
||||
</template>
|
||||
|
||||
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :class="wrapperStyle" @click="focusInput">
|
||||
<i class="icon-[lucide--search] text-muted" />
|
||||
<i class="icon-[lucide--search] text-muted-foreground" />
|
||||
<InputText
|
||||
ref="input"
|
||||
v-model="internalSearchQuery"
|
||||
@@ -73,7 +73,7 @@ onMounted(() => autofocus && focusInput())
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const baseClasses =
|
||||
'relative flex w-full items-center gap-2 bg-base-background cursor-text'
|
||||
'relative flex w-full items-center gap-2 bg-secondary-background cursor-text'
|
||||
|
||||
if (showBorder) {
|
||||
return cn(
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
'h-10 relative inline-flex cursor-pointer select-none items-center',
|
||||
// trigger surface
|
||||
'rounded-lg',
|
||||
'bg-base-background text-base-foreground',
|
||||
'bg-secondary-background text-base-foreground',
|
||||
'border-[2.5px] border-solid border-transparent',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'focus-within:border-node-component-border',
|
||||
@@ -84,7 +84,7 @@
|
||||
>
|
||||
<!-- Trigger value -->
|
||||
<template #value="slotProps">
|
||||
<div class="flex items-center gap-2 text-sm text-neutral-500">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<slot name="icon" />
|
||||
<span
|
||||
v-if="slotProps.value !== null && slotProps.value !== undefined"
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
<!-- Trigger caret -->
|
||||
<template #dropdownicon>
|
||||
<i class="icon-[lucide--chevron-down] text-base text-neutral-500" />
|
||||
<i class="icon-[lucide--chevron-down] text-muted-foreground" />
|
||||
</template>
|
||||
|
||||
<!-- Option row -->
|
||||
@@ -152,7 +152,7 @@ const {
|
||||
popoverMaxWidth?: string
|
||||
}>()
|
||||
|
||||
const selectedItem = defineModel<string | null>({ required: true })
|
||||
const selectedItem = defineModel<string | undefined>({ required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import {
|
||||
getEffectiveBrushSize,
|
||||
getEffectiveHardness
|
||||
} from '@/composables/maskeditor/brushUtils'
|
||||
import { BrushShape } from '@/extensions/core/maskeditor/types'
|
||||
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
||||
|
||||
@@ -36,11 +40,14 @@ const { containerRef } = defineProps<{
|
||||
const store = useMaskEditorStore()
|
||||
|
||||
const brushOpacity = computed(() => {
|
||||
return store.brushVisible ? '1' : '0'
|
||||
return store.brushVisible ? 1 : 0
|
||||
})
|
||||
|
||||
const brushRadius = computed(() => {
|
||||
return store.brushSettings.size * store.zoomRatio
|
||||
const size = store.brushSettings.size
|
||||
const hardness = store.brushSettings.hardness
|
||||
const effectiveSize = getEffectiveBrushSize(size, hardness)
|
||||
return effectiveSize * store.zoomRatio
|
||||
})
|
||||
|
||||
const brushSize = computed(() => {
|
||||
@@ -78,19 +85,26 @@ const gradientVisible = computed(() => {
|
||||
})
|
||||
|
||||
const gradientBackground = computed(() => {
|
||||
const size = store.brushSettings.size
|
||||
const hardness = store.brushSettings.hardness
|
||||
const effectiveSize = getEffectiveBrushSize(size, hardness)
|
||||
const effectiveHardness = getEffectiveHardness(size, hardness, effectiveSize)
|
||||
|
||||
if (hardness === 1) {
|
||||
if (effectiveHardness === 1) {
|
||||
return 'rgba(255, 0, 0, 0.5)'
|
||||
}
|
||||
|
||||
const midStop = hardness * 100
|
||||
const midStop = effectiveHardness * 100
|
||||
const outerStop = 100
|
||||
// Add an intermediate stop to approximate the squared falloff
|
||||
// At 50% of the fade region, squared falloff is 0.25 (relative to max)
|
||||
const fadeMidStop = midStop + (outerStop - midStop) * 0.5
|
||||
|
||||
return `radial-gradient(
|
||||
circle,
|
||||
rgba(255, 0, 0, 0.5) 0%,
|
||||
rgba(255, 0, 0, 0.25) ${midStop}%,
|
||||
rgba(255, 0, 0, 0.5) ${midStop}%,
|
||||
rgba(255, 0, 0, 0.125) ${fadeMidStop}%,
|
||||
rgba(255, 0, 0, 0) ${outerStop}%
|
||||
)`
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<SliderControl
|
||||
:label="t('maskEditor.thickness')"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:max="500"
|
||||
:step="1"
|
||||
:model-value="store.brushSettings.size"
|
||||
@update:model-value="onThicknessChange"
|
||||
@@ -80,12 +80,12 @@
|
||||
/>
|
||||
|
||||
<SliderControl
|
||||
:label="t('maskEditor.smoothingPrecision')"
|
||||
:label="$t('maskEditor.stepSize')"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:model-value="store.brushSettings.smoothingPrecision"
|
||||
@update:model-value="onSmoothingPrecisionChange"
|
||||
:model-value="store.brushSettings.stepSize"
|
||||
@update:model-value="onStepSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -119,8 +119,8 @@ const onHardnessChange = (value: number) => {
|
||||
store.setBrushHardness(value)
|
||||
}
|
||||
|
||||
const onSmoothingPrecisionChange = (value: number) => {
|
||||
store.setBrushSmoothingPrecision(value)
|
||||
const onStepSizeChange = (value: number) => {
|
||||
store.setBrushStepSize(value)
|
||||
}
|
||||
|
||||
const resetToDefault = () => {
|
||||
|
||||
@@ -12,19 +12,28 @@
|
||||
>
|
||||
<canvas
|
||||
ref="imgCanvasRef"
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
class="absolute top-0 left-0 w-full h-full z-0"
|
||||
@contextmenu.prevent
|
||||
/>
|
||||
<canvas
|
||||
ref="rgbCanvasRef"
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
class="absolute top-0 left-0 w-full h-full z-10"
|
||||
@contextmenu.prevent
|
||||
/>
|
||||
<canvas
|
||||
ref="maskCanvasRef"
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
class="absolute top-0 left-0 w-full h-full z-30"
|
||||
@contextmenu.prevent
|
||||
/>
|
||||
<!-- GPU Preview Canvas -->
|
||||
<canvas
|
||||
ref="gpuCanvasRef"
|
||||
class="absolute top-0 left-0 w-full h-full pointer-events-none"
|
||||
:class="{
|
||||
'z-20': store.activeLayer === 'rgb',
|
||||
'z-40': store.activeLayer === 'mask'
|
||||
}"
|
||||
/>
|
||||
<div ref="canvasBackgroundRef" class="bg-white w-full h-full" />
|
||||
</div>
|
||||
|
||||
@@ -87,6 +96,7 @@ const canvasContainerRef = ref<HTMLDivElement>()
|
||||
const imgCanvasRef = ref<HTMLCanvasElement>()
|
||||
const maskCanvasRef = ref<HTMLCanvasElement>()
|
||||
const rgbCanvasRef = ref<HTMLCanvasElement>()
|
||||
const gpuCanvasRef = ref<HTMLCanvasElement>()
|
||||
const canvasBackgroundRef = ref<HTMLDivElement>()
|
||||
|
||||
const toolPanelRef = ref<InstanceType<typeof ToolPanel>>()
|
||||
@@ -97,7 +107,7 @@ const initialized = ref(false)
|
||||
const keyboard = useKeyboard()
|
||||
const panZoom = usePanAndZoom()
|
||||
|
||||
let toolManager: ReturnType<typeof useToolManager> | null = null
|
||||
const toolManager = useToolManager(keyboard, panZoom)
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
@@ -135,8 +145,6 @@ const initUI = async () => {
|
||||
try {
|
||||
await loader.loadFromNode(node)
|
||||
|
||||
toolManager = useToolManager(keyboard, panZoom)
|
||||
|
||||
const imageLoader = useImageLoader()
|
||||
const image = await imageLoader.loadImages()
|
||||
|
||||
@@ -149,6 +157,18 @@ const initUI = async () => {
|
||||
|
||||
store.canvasHistory.saveInitialState()
|
||||
|
||||
// Initialize GPU resources
|
||||
if (toolManager.brushDrawing) {
|
||||
await toolManager.brushDrawing.initGPUResources()
|
||||
if (gpuCanvasRef.value && toolManager.brushDrawing.initPreviewCanvas) {
|
||||
// Match preview canvas resolution to mask canvas
|
||||
gpuCanvasRef.value.width = maskCanvasRef.value.width
|
||||
gpuCanvasRef.value.height = maskCanvasRef.value.height
|
||||
|
||||
toolManager.brushDrawing.initPreviewCanvas(gpuCanvasRef.value)
|
||||
}
|
||||
}
|
||||
|
||||
initialized.value = true
|
||||
} catch (error) {
|
||||
console.error('[MaskEditorContent] Initialization failed:', error)
|
||||
@@ -172,7 +192,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
toolManager?.brushDrawing.saveBrushSettings()
|
||||
toolManager.brushDrawing.saveBrushSettings()
|
||||
|
||||
keyboard?.removeListeners()
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ const onInvert = () => {
|
||||
|
||||
const onClear = () => {
|
||||
canvasTools.clearMask()
|
||||
store.triggerClear()
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
-->
|
||||
<template>
|
||||
<LGraphNodePreview v-if="shouldRenderVueNodes" :node-def="nodeDef" />
|
||||
<div v-else class="_sb_node_preview">
|
||||
<div v-else class="_sb_node_preview bg-component-node-background">
|
||||
<div class="_sb_table">
|
||||
<div
|
||||
class="node_header mr-4 text-ellipsis"
|
||||
@@ -200,7 +200,6 @@ const truncateDefaultValue = (value: any, charLimit: number = 32): string => {
|
||||
}
|
||||
|
||||
._sb_node_preview {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: var(--descrip-text);
|
||||
border: 1px solid var(--descrip-text);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="group flex w-full items-center justify-between gap-3 rounded-lg border-0 bg-secondary-background p-1 text-left transition-colors duration-200 ease-in-out hover:cursor-pointer hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
<IconButton
|
||||
type="secondary"
|
||||
size="fit-content"
|
||||
class="group w-full justify-between gap-3 rounded-lg p-1 text-left font-normal hover:cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="props.ariaLabel"
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span v-if="props.mode === 'allFailed'" class="inline-flex items-center">
|
||||
@@ -23,7 +25,11 @@
|
||||
class="inline-block h-6 w-6 overflow-hidden rounded-[6px] border-0 bg-secondary-background"
|
||||
:style="{ marginLeft: idx === 0 ? '0' : '-12px' }"
|
||||
>
|
||||
<img :src="url" alt="preview" class="h-full w-full object-cover" />
|
||||
<img
|
||||
:src="url"
|
||||
:alt="$t('sideToolbar.queueProgressOverlay.preview')"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -76,10 +82,11 @@
|
||||
>
|
||||
<i class="icon-[lucide--chevron-down] block size-4 leading-none" />
|
||||
</span>
|
||||
</button>
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import type {
|
||||
CompletionSummary,
|
||||
CompletionSummaryMode
|
||||
@@ -96,4 +103,8 @@ type Props = {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
thumbnailUrls: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', event: MouseEvent): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -42,17 +42,19 @@
|
||||
t('sideToolbar.queueProgressOverlay.running')
|
||||
}}</span>
|
||||
</span>
|
||||
<button
|
||||
<IconButton
|
||||
v-if="runningCount > 0"
|
||||
v-tooltip.top="cancelJobTooltip"
|
||||
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 transition-colors hover:bg-destructive-background"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="size-6 bg-destructive-background hover:bg-destructive-background-hover"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
|
||||
@click="$emit('interruptAll')"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -62,26 +64,28 @@
|
||||
t('sideToolbar.queueProgressOverlay.queuedSuffix')
|
||||
}}</span>
|
||||
</span>
|
||||
<button
|
||||
<IconButton
|
||||
v-if="queuedCount > 0"
|
||||
v-tooltip.top="clearQueueTooltip"
|
||||
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 transition-colors hover:bg-destructive-background"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="size-6 bg-secondary-background hover:bg-destructive-background"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
@click="$emit('clearQueued')"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="inline-flex h-6 min-w-[120px] flex-1 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background px-2 py-0 text-[12px] text-text-primary hover:bg-secondary-background-hover hover:opacity-90"
|
||||
<TextButton
|
||||
class="h-6 min-w-[120px] flex-1 px-2 py-0 text-[12px]"
|
||||
type="secondary"
|
||||
:label="t('sideToolbar.queueProgressOverlay.viewAllJobs')"
|
||||
@click="$emit('viewAllJobs')"
|
||||
>
|
||||
{{ t('sideToolbar.queueProgressOverlay.viewAllJobs') }}
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -90,6 +94,8 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -8,17 +8,20 @@
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between px-3">
|
||||
<button
|
||||
class="inline-flex grow cursor-pointer items-center justify-center gap-1 rounded border-0 bg-secondary-background p-2 text-center font-inter text-[12px] leading-none text-text-primary hover:bg-secondary-background-hover hover:opacity-90"
|
||||
<IconTextButton
|
||||
class="grow gap-1 p-2 text-center font-inter text-[12px] leading-none hover:opacity-90 justify-center"
|
||||
type="secondary"
|
||||
:label="t('sideToolbar.queueProgressOverlay.showAssets')"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.showAssets')"
|
||||
@click="$emit('showAssets')"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none block size-4 shrink-0 leading-none icon-[comfy--image-ai-edit]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ t('sideToolbar.queueProgressOverlay.showAssets') }}</span>
|
||||
</button>
|
||||
<template #icon>
|
||||
<div
|
||||
class="pointer-events-none block size-4 shrink-0 leading-none icon-[comfy--image-ai-edit]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<div class="ml-4 inline-flex items-center">
|
||||
<div
|
||||
class="inline-flex h-6 items-center text-[12px] leading-none text-text-primary opacity-90"
|
||||
@@ -28,16 +31,18 @@
|
||||
t('sideToolbar.queueProgressOverlay.queuedSuffix')
|
||||
}}</span>
|
||||
</div>
|
||||
<button
|
||||
<IconButton
|
||||
v-if="queuedCount > 0"
|
||||
class="group ml-2 inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 transition-colors hover:bg-destructive-background"
|
||||
class="group ml-2 size-6 bg-secondary-background hover:bg-destructive-background"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
@click="$emit('clearQueued')"
|
||||
>
|
||||
<i
|
||||
class="pointer-events-none icon-[lucide--list-x] block size-4 leading-none text-text-primary transition-colors group-hover:text-base-background"
|
||||
/>
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,6 +80,8 @@
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import type {
|
||||
JobGroup,
|
||||
JobListItem,
|
||||
|
||||
@@ -18,16 +18,18 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
<IconButton
|
||||
v-tooltip.top="moreTooltipConfig"
|
||||
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-transparent p-0 hover:bg-secondary-background hover:opacity-100"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 bg-transparent hover:bg-secondary-background hover:opacity-100"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.moreOptions')"
|
||||
@click="onMoreClick"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</button>
|
||||
</IconButton>
|
||||
<Popover
|
||||
ref="morePopoverRef"
|
||||
:dismissable="true"
|
||||
@@ -45,18 +47,19 @@
|
||||
<div
|
||||
class="flex flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3 font-inter"
|
||||
>
|
||||
<button
|
||||
class="inline-flex w-full cursor-pointer items-center justify-start gap-2 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
<IconTextButton
|
||||
class="w-full justify-start gap-2 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
type="transparent"
|
||||
:label="t('sideToolbar.queueProgressOverlay.clearHistory')"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearHistory')"
|
||||
@click="onClearHistoryFromMenu"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--file-x-2] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.clearHistory')
|
||||
}}</span>
|
||||
</button>
|
||||
<template #icon>
|
||||
<i
|
||||
class="icon-[lucide--file-x-2] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
@@ -69,6 +72,8 @@ import type { PopoverMethods } from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
defineProps<{
|
||||
|
||||