Compare commits
54 Commits
command-bo
...
issue-5033
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11098f7f9b | ||
|
|
65785af348 | ||
|
|
ec4ad5ea92 | ||
|
|
e9ddf29507 | ||
|
|
fdd8564c07 | ||
|
|
d18081a54e | ||
|
|
45cc6ca2b4 | ||
|
|
c303a3f037 | ||
|
|
c90fd18ade | ||
|
|
2ed1704749 | ||
|
|
7d5a4d423e | ||
|
|
7aaa0f022e | ||
|
|
a132dad216 | ||
|
|
9dbdc6a72b | ||
|
|
7b228d693d | ||
|
|
547af0e043 | ||
|
|
4ca6220adf | ||
|
|
1e41c6dc45 | ||
|
|
5224c63bce | ||
|
|
89c78b0ecb | ||
|
|
4a3bd39650 | ||
|
|
db1b81b7ff | ||
|
|
5e81343142 | ||
|
|
6566acb406 | ||
|
|
efc0431a5e | ||
|
|
1784e2b5a3 | ||
|
|
39df4ac9da | ||
|
|
eba0b42674 | ||
|
|
ef1852d551 | ||
|
|
983ebb2ba7 | ||
|
|
db71365768 | ||
|
|
17d7ba8bcb | ||
|
|
24a386c766 | ||
|
|
c42c9315f4 | ||
|
|
d068b8351e | ||
|
|
1cf8087be0 | ||
|
|
9c31d708a2 | ||
|
|
9a70e927aa | ||
|
|
dc444faa75 | ||
|
|
a055ec2dff | ||
|
|
2138ceea80 | ||
|
|
7972550f6b | ||
|
|
c7baf3c340 | ||
|
|
8403bd0e3e | ||
|
|
90f54414ab | ||
|
|
505c242ff4 | ||
|
|
fbc6edde25 | ||
|
|
2c215a6251 | ||
|
|
71a43193df | ||
|
|
d0d13bfe4c | ||
|
|
a1a8d48544 | ||
|
|
d22d62b670 | ||
|
|
8e357c41e3 | ||
|
|
c4912dcd54 |
@@ -210,29 +210,52 @@ echo "Last stable release: $LAST_STABLE"
|
||||
echo "WARNING: PR #$PR not on main branch!"
|
||||
done
|
||||
```
|
||||
3. Create comprehensive release notes including:
|
||||
- **Version Change**: Show version bump details
|
||||
- **Changelog** grouped by type:
|
||||
- 🚀 **Features** (feat:)
|
||||
- 🐛 **Bug Fixes** (fix:)
|
||||
- 💥 **Breaking Changes** (BREAKING CHANGE)
|
||||
- 📚 **Documentation** (docs:)
|
||||
- 🔧 **Maintenance** (chore:, refactor:)
|
||||
- ⬆️ **Dependencies** (deps:, dependency updates)
|
||||
- **Litegraph Changes** (if version updated):
|
||||
- 🚀 Features: ${LITEGRAPH_FEATURES}
|
||||
- 🐛 Bug Fixes: ${LITEGRAPH_FIXES}
|
||||
- 💥 Breaking Changes: ${LITEGRAPH_BREAKING}
|
||||
- 🔧 Other Changes: ${LITEGRAPH_OTHER}
|
||||
- **Other Major Dependencies**: ${OTHER_DEP_CHANGES}
|
||||
- Include PR numbers and links
|
||||
- Add issue references (Fixes #123)
|
||||
4. **Save release notes:**
|
||||
3. Create standardized release notes using this exact template:
|
||||
```bash
|
||||
# Save release notes for PR and GitHub release
|
||||
echo "$RELEASE_NOTES" > release-notes-${NEW_VERSION}.md
|
||||
cat > release-notes-${NEW_VERSION}.md << 'EOF'
|
||||
## ⚠️ Breaking Changes
|
||||
<!-- List breaking changes if any, otherwise remove this entire section -->
|
||||
- Breaking change description (#PR_NUMBER)
|
||||
|
||||
---
|
||||
|
||||
## What's Changed
|
||||
|
||||
### 🚀 Features
|
||||
<!-- List features here, one per line with PR reference -->
|
||||
- Feature description (#PR_NUMBER)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
<!-- List bug fixes here, one per line with PR reference -->
|
||||
- Bug fix description (#PR_NUMBER)
|
||||
|
||||
### 🔧 Maintenance
|
||||
<!-- List refactoring, chore, and other maintenance items -->
|
||||
- Maintenance item description (#PR_NUMBER)
|
||||
|
||||
### 📚 Documentation
|
||||
<!-- List documentation changes if any, remove section if empty -->
|
||||
- Documentation update description (#PR_NUMBER)
|
||||
|
||||
### ⬆️ Dependencies
|
||||
<!-- List dependency updates -->
|
||||
- Updated dependency from vX.X.X to vY.Y.Y (#PR_NUMBER)
|
||||
|
||||
**Full Changelog**: https://github.com/Comfy-Org/ComfyUI_frontend/compare/${BASE_TAG}...v${NEW_VERSION}
|
||||
EOF
|
||||
```
|
||||
5. **CONTENT REVIEW**: Release notes clear and comprehensive with dependency details?
|
||||
4. **Parse commits and populate template:**
|
||||
- Group commits by conventional commit type (feat:, fix:, chore:, etc.)
|
||||
- Extract PR numbers from commit messages
|
||||
- For breaking changes, analyze if changes affect:
|
||||
- Public APIs (app object, api module)
|
||||
- Extension/workspace manager APIs
|
||||
- Node schema, workflow schema, or other public schemas
|
||||
- Any other public-facing interfaces
|
||||
- For dependency updates, list version changes with PR numbers
|
||||
- Remove empty sections (e.g., if no documentation changes)
|
||||
- Ensure consistent bullet format: `- Description (#PR_NUMBER)`
|
||||
5. **CONTENT REVIEW**: Release notes follow standard format?
|
||||
|
||||
### Step 9: Create Version Bump PR
|
||||
|
||||
@@ -273,38 +296,12 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
--body-file release-notes-${NEW_VERSION}.md \
|
||||
--label "Release"
|
||||
```
|
||||
3. **Add required sections to PR body:**
|
||||
3. **Update PR with release notes:**
|
||||
```bash
|
||||
# Create PR body with release notes plus required sections
|
||||
cat > pr-body.md << EOF
|
||||
${RELEASE_NOTES}
|
||||
|
||||
## Breaking Changes
|
||||
${BREAKING_CHANGES:-None}
|
||||
|
||||
## Testing Performed
|
||||
- ✅ Full test suite (unit, component)
|
||||
- ✅ TypeScript compilation
|
||||
- ✅ Linting checks
|
||||
- ✅ Build verification
|
||||
- ✅ Security audit
|
||||
|
||||
## Distribution Channels
|
||||
- GitHub Release (with dist.zip)
|
||||
- PyPI Package (comfyui-frontend-package)
|
||||
- npm Package (@comfyorg/comfyui-frontend-types)
|
||||
|
||||
## Post-Release Tasks
|
||||
- [ ] Verify all distribution channels
|
||||
- [ ] Update external documentation
|
||||
- [ ] Monitor for issues
|
||||
EOF
|
||||
# For workflow-created PRs, update the body with our release notes
|
||||
gh pr edit ${PR_NUMBER} --body-file release-notes-${NEW_VERSION}.md
|
||||
```
|
||||
4. Update PR with enhanced description:
|
||||
```bash
|
||||
gh pr edit ${PR_NUMBER} --body-file pr-body.md
|
||||
```
|
||||
5. **PR REVIEW**: Version bump PR created and enhanced correctly?
|
||||
4. **PR REVIEW**: Version bump PR created with standardized release notes?
|
||||
|
||||
### Step 10: Critical Release PR Verification
|
||||
|
||||
@@ -505,11 +502,94 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
## Files Generated
|
||||
- \`release-notes-${NEW_VERSION}.md\` - Comprehensive release notes
|
||||
- \`post-release-checklist.md\` - Follow-up tasks
|
||||
- \`gtm-summary-${NEW_VERSION}.md\` - Marketing team notification
|
||||
EOF
|
||||
```
|
||||
|
||||
4. **RELEASE COMPLETION**: All post-release setup completed?
|
||||
|
||||
### Step 16: Generate GTM Feature Summary
|
||||
|
||||
1. **Extract and analyze PR data:**
|
||||
```bash
|
||||
echo "📊 Checking for marketing-worthy features..."
|
||||
|
||||
# Extract all PR data inline
|
||||
PR_DATA=$(
|
||||
PR_LIST=$(git log ${BASE_TAG}..HEAD --grep="Merge pull request" --pretty=format:"%s" | grep -oE "#[0-9]+" | tr -d '#' | sort -u)
|
||||
|
||||
echo "["
|
||||
first=true
|
||||
for PR in $PR_LIST; do
|
||||
[[ "$first" == true ]] && first=false || echo ","
|
||||
gh pr view $PR --json number,title,author,body,files,labels,closedAt 2>/dev/null || continue
|
||||
done
|
||||
echo "]"
|
||||
)
|
||||
|
||||
# Save for analysis
|
||||
echo "$PR_DATA" > prs-${NEW_VERSION}.json
|
||||
```
|
||||
|
||||
2. **Analyze for GTM-worthy features:**
|
||||
```
|
||||
<task>
|
||||
Review these PRs to identify if ANY would interest a marketing/growth team.
|
||||
|
||||
Consider if a PR:
|
||||
- Changes something users directly interact with or experience
|
||||
- Makes something noticeably better, faster, or easier
|
||||
- Introduces capabilities users have been asking for
|
||||
- Has visual assets (screenshots, GIFs, videos) that could be shared
|
||||
- Tells a compelling story about improvement or innovation
|
||||
- Would make users excited if they heard about it
|
||||
|
||||
Many releases contain only technical improvements, bug fixes, or internal changes -
|
||||
that's perfectly normal. Only flag PRs that would genuinely interest end users.
|
||||
|
||||
If you find marketing-worthy PRs, note:
|
||||
- PR number, title, and author
|
||||
- Any media links from the description
|
||||
- One sentence on why it's worth showcasing
|
||||
|
||||
If nothing is marketing-worthy, just say "No marketing-worthy features in this release."
|
||||
</task>
|
||||
|
||||
PR data: [contents of prs-${NEW_VERSION}.json]
|
||||
```
|
||||
|
||||
3. **Generate GTM notification (only if needed):**
|
||||
```
|
||||
If there are marketing-worthy features, create a message for #gtm with:
|
||||
|
||||
🚀 Frontend Release v${NEW_VERSION}
|
||||
|
||||
Timeline: Available now in nightly, ~2-3 weeks for core
|
||||
|
||||
Features worth showcasing:
|
||||
[List the selected PRs with media links and authors]
|
||||
|
||||
Testing: --front-end-version ${NEW_VERSION}
|
||||
|
||||
If there are NO marketing-worthy features, generate:
|
||||
"No marketing-worthy features in v${NEW_VERSION} - mostly internal improvements and bug fixes."
|
||||
```
|
||||
|
||||
4. **Save the output:**
|
||||
```bash
|
||||
# Claude generates the GTM summary and saves it
|
||||
# Save to gtm-summary-${NEW_VERSION}.md
|
||||
|
||||
# Check if notification is needed
|
||||
if grep -q "No marketing-worthy features" gtm-summary-${NEW_VERSION}.md; then
|
||||
echo "✅ No GTM notification needed for this release"
|
||||
echo "📄 Summary saved to: gtm-summary-${NEW_VERSION}.md"
|
||||
else
|
||||
echo "📋 GTM summary saved to: gtm-summary-${NEW_VERSION}.md"
|
||||
echo "📤 Share this file in #gtm channel to notify the team"
|
||||
fi
|
||||
```
|
||||
|
||||
## Advanced Safety Features
|
||||
|
||||
### Rollback Procedures
|
||||
@@ -592,55 +672,46 @@ The command implements multiple quality gates:
|
||||
- Draft release status
|
||||
- Python package specs require that prereleases use alpha/beta/rc as the preid
|
||||
|
||||
## Common Issues and Solutions
|
||||
## Critical Implementation Notes
|
||||
|
||||
### Issue: Pre-release Version Confusion
|
||||
**Problem**: Not sure whether to promote pre-release or create new version
|
||||
**Solution**:
|
||||
- Follow semver standards: a prerelease version is followed by a normal release. It should have the same major, minor, and patch versions as the prerelease.
|
||||
When executing this release process, pay attention to these key aspects:
|
||||
|
||||
### Issue: Wrong Commit Count
|
||||
**Problem**: Changelog includes commits from other branches
|
||||
**Solution**: Always use `--first-parent` flag with git log
|
||||
### Version Handling
|
||||
- For pre-release versions (e.g., 1.24.0-rc.1), the next stable release should be the same version without the suffix (1.24.0)
|
||||
- Never skip version numbers - follow semantic versioning strictly
|
||||
|
||||
**Update**: Sometimes update-locales doesn't add [skip ci] - always verify!
|
||||
### Commit History Analysis
|
||||
- **ALWAYS** use `--first-parent` flag with git log to avoid including commits from merged feature branches
|
||||
- Verify PR merge targets before including them in changelogs:
|
||||
```bash
|
||||
gh pr view ${PR_NUMBER} --json baseRefName
|
||||
```
|
||||
|
||||
### Issue: Missing PRs in Changelog
|
||||
**Problem**: PR was merged to different branch
|
||||
**Solution**: Verify PR merge target with:
|
||||
```bash
|
||||
gh pr view ${PR_NUMBER} --json baseRefName
|
||||
```
|
||||
### Release Workflow Triggers
|
||||
- The "Release" label on the PR is **CRITICAL** - without it, PyPI/npm publishing won't occur
|
||||
- Check for `[skip ci]` in commit messages before merging - this blocks the release workflow
|
||||
- If you encounter `[skip ci]`, push an empty commit to override it:
|
||||
```bash
|
||||
git commit --allow-empty -m "Trigger release workflow"
|
||||
```
|
||||
|
||||
### Issue: Incomplete Dependency Changelog
|
||||
**Problem**: Litegraph or other dependency updates only show version bump, not actual changes
|
||||
**Solution**: The command now automatically:
|
||||
- Detects litegraph version changes between releases
|
||||
- Clones the litegraph repository temporarily
|
||||
- Extracts and categorizes changes between versions
|
||||
- Includes detailed litegraph changelog in release notes
|
||||
- Cleans up temporary files after analysis
|
||||
### PR Creation Details
|
||||
- Version bump PRs come from `comfy-pr-bot`, not `github-actions`
|
||||
- The workflow typically completes in 20-30 seconds
|
||||
- Always wait for the PR to be created before trying to edit it
|
||||
|
||||
### Issue: Release Failed Due to [skip ci]
|
||||
**Problem**: Release workflow didn't trigger after merge
|
||||
**Prevention**: Always avoid this scenario
|
||||
- Ensure that `[skip ci]` or similar flags are NOT in the `HEAD` commit message of the PR
|
||||
- Push a new, empty commit to the PR
|
||||
- Always double-check this immediately before merging
|
||||
### Breaking Changes Detection
|
||||
- Analyze changes to public-facing APIs:
|
||||
- The `app` object and its methods
|
||||
- The `api` module exports
|
||||
- Extension and workspace manager interfaces
|
||||
- Node schema, workflow schema, and other public schemas
|
||||
- Any modifications to these require marking as breaking changes
|
||||
|
||||
**Recovery Strategy**:
|
||||
1. Revert version in a new PR (e.g., 1.24.0 → 1.24.0-1)
|
||||
2. Merge the revert PR
|
||||
3. Run version bump workflow again
|
||||
4. This creates a fresh PR without [skip ci]
|
||||
Benefits: Cleaner than creating extra version numbers
|
||||
|
||||
## Key Learnings & Notes
|
||||
|
||||
1. **PR Author**: Version bump PRs are created by `comfy-pr-bot`, not `github-actions`
|
||||
2. **Workflow Speed**: Version bump workflow typically completes in ~20-30 seconds
|
||||
3. **Update-locales Behavior**: Inconsistent - sometimes adds [skip ci], sometimes doesn't
|
||||
4. **Recovery Options**: Reverting version is cleaner than creating extra versions
|
||||
5. **Dependency Tracking**: Command now automatically includes litegraph and major dependency changes in changelogs
|
||||
6. **Litegraph Integration**: Temporary cloning of litegraph repo provides detailed change analysis between versions
|
||||
### Recovery Procedures
|
||||
If the release workflow fails to trigger:
|
||||
1. Create a revert PR to restore the previous version
|
||||
2. Merge the revert
|
||||
3. Re-run the version bump workflow
|
||||
4. This approach is cleaner than creating extra version numbers
|
||||
|
||||
|
||||
@@ -138,14 +138,50 @@ For each commit:
|
||||
```bash
|
||||
gh pr create --base core/X.Y --head release/1.23.5 \
|
||||
--title "[Release] v1.23.5" \
|
||||
--body "..." \
|
||||
--body "Release notes will be added shortly..." \
|
||||
--label "Release"
|
||||
```
|
||||
3. **CRITICAL**: Verify "Release" label is added
|
||||
4. PR description should include:
|
||||
- Version: `1.23.4` → `1.23.5`
|
||||
- Included fixes (link to previous PR)
|
||||
- Release notes for users
|
||||
4. Create standardized release notes:
|
||||
```bash
|
||||
cat > release-notes-${NEW_VERSION}.md << 'EOF'
|
||||
## ⚠️ Breaking Changes
|
||||
<!-- List breaking changes if any, otherwise remove this entire section -->
|
||||
- Breaking change description (#PR_NUMBER)
|
||||
|
||||
---
|
||||
|
||||
## What's Changed
|
||||
|
||||
### 🚀 Features
|
||||
<!-- List features here, one per line with PR reference -->
|
||||
- Feature description (#PR_NUMBER)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
<!-- List bug fixes here, one per line with PR reference -->
|
||||
- Bug fix description (#PR_NUMBER)
|
||||
|
||||
### 🔧 Maintenance
|
||||
<!-- List refactoring, chore, and other maintenance items -->
|
||||
- Maintenance item description (#PR_NUMBER)
|
||||
|
||||
### 📚 Documentation
|
||||
<!-- List documentation changes if any, remove section if empty -->
|
||||
- Documentation update description (#PR_NUMBER)
|
||||
|
||||
### ⬆️ Dependencies
|
||||
<!-- List dependency updates -->
|
||||
- Updated dependency from vX.X.X to vY.Y.Y (#PR_NUMBER)
|
||||
|
||||
**Full Changelog**: https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${CURRENT_VERSION}...v${NEW_VERSION}
|
||||
EOF
|
||||
```
|
||||
- For hotfixes, typically only populate the "Bug Fixes" section
|
||||
- Include links to the cherry-picked PRs/commits
|
||||
- Update the PR body with the release notes:
|
||||
```bash
|
||||
gh pr edit ${PR_NUMBER} --body-file release-notes-${NEW_VERSION}.md
|
||||
```
|
||||
5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
|
||||
|
||||
### Step 11: Monitor Release Process
|
||||
|
||||
131
.claude/commands/pr.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Create PR
|
||||
|
||||
Automate PR creation with proper tags, labels, and concise summary.
|
||||
|
||||
## Step 1: Check Prerequisites
|
||||
|
||||
```bash
|
||||
# Ensure you have uncommitted changes
|
||||
git status
|
||||
|
||||
# If changes exist, commit them first
|
||||
git add .
|
||||
git commit -m "[tag] Your commit message"
|
||||
```
|
||||
|
||||
## Step 2: Push and Create PR
|
||||
|
||||
You'll create the PR with the following structure:
|
||||
|
||||
### PR Tags (use in title)
|
||||
|
||||
- `[feat]` - New features → label: `enhancement`
|
||||
- `[bugfix]` - Bug fixes → label: `verified bug`
|
||||
- `[refactor]` - Code restructuring → label: `enhancement`
|
||||
- `[docs]` - Documentation → label: `documentation`
|
||||
- `[test]` - Test changes → label: `enhancement`
|
||||
- `[ci]` - CI/CD changes → label: `enhancement`
|
||||
|
||||
### Label Mapping
|
||||
|
||||
#### General Labels
|
||||
|
||||
- Feature/Enhancement: `enhancement`
|
||||
- Bug fixes: `verified bug`
|
||||
- Documentation: `documentation`
|
||||
- Dependencies: `dependencies`
|
||||
- Performance: `Performance`
|
||||
- Desktop app: `Electron`
|
||||
|
||||
#### Product Area Labels
|
||||
|
||||
**Core Features**
|
||||
|
||||
- `area:nodes` - Node-related functionality
|
||||
- `area:workflows` - Workflow management
|
||||
- `area:queue` - Queue system
|
||||
- `area:models` - Model handling
|
||||
- `area:templates` - Template system
|
||||
- `area:subgraph` - Subgraph functionality
|
||||
|
||||
**UI Components**
|
||||
|
||||
- `area:ui` - General user interface improvements
|
||||
- `area:widgets` - Widget system
|
||||
- `area:dom-widgets` - DOM-based widgets
|
||||
- `area:links` - Connection links between nodes
|
||||
- `area:groups` - Node grouping functionality
|
||||
- `area:reroutes` - Reroute nodes
|
||||
- `area:previews` - Preview functionality
|
||||
- `area:minimap` - Minimap navigation
|
||||
- `area:floating-toolbox` - Floating toolbar
|
||||
- `area:mask-editor` - Mask editing tools
|
||||
|
||||
**Navigation & Organization**
|
||||
|
||||
- `area:navigation` - Navigation system
|
||||
- `area:search` - Search functionality
|
||||
- `area:workspace-management` - Workspace features
|
||||
- `area:topbar-menu` - Top bar menu
|
||||
- `area:help-menu` - Help menu system
|
||||
|
||||
**System Features**
|
||||
|
||||
- `area:settings` - Settings/preferences
|
||||
- `area:hotkeys` - Keyboard shortcuts
|
||||
- `area:undo-redo` - Undo/redo system
|
||||
- `area:customization` - Customization features
|
||||
- `area:auth` - Authentication
|
||||
- `area:comms` - Communication/networking
|
||||
|
||||
**Development & Infrastructure**
|
||||
|
||||
- `area:CI/CD` - CI/CD pipeline
|
||||
- `area:testing` - Testing infrastructure
|
||||
- `area:vue-migration` - Vue migration work
|
||||
- `area:manager` - ComfyUI Manager integration
|
||||
|
||||
**Platform-Specific**
|
||||
|
||||
- `area:mobile` - Mobile support
|
||||
- `area:3d` - 3D-related features
|
||||
|
||||
**Special Areas**
|
||||
|
||||
- `area:i18n` - Translation/internationalization
|
||||
- `area:CNR` - Comfy Node Registry
|
||||
|
||||
## Step 3: Execute PR Creation
|
||||
|
||||
```bash
|
||||
# First, push your branch
|
||||
git push -u origin $(git branch --show-current)
|
||||
|
||||
# Then create the PR (replace placeholders)
|
||||
gh pr create \
|
||||
--title "[TAG] Brief description" \
|
||||
--body "$(cat <<'EOF'
|
||||
## Summary
|
||||
One sentence describing what changed and why.
|
||||
|
||||
## Changes
|
||||
- **What**: Core functionality added/modified
|
||||
- **Breaking**: Any breaking changes (if none, omit this line)
|
||||
- **Dependencies**: New dependencies (if none, omit this line)
|
||||
|
||||
## Review Focus
|
||||
- Critical design decisions or edge cases that need attention
|
||||
|
||||
Fixes #ISSUE_NUMBER
|
||||
EOF
|
||||
)" \
|
||||
--label "APPROPRIATE_LABEL" \
|
||||
--base main
|
||||
```
|
||||
|
||||
## Additional Options
|
||||
|
||||
- Add multiple labels: `--label "enhancement,Performance"`
|
||||
- Request reviewers: `--reviewer @username`
|
||||
- Mark as draft: `--draft`
|
||||
- Open in browser after creation: `--web`
|
||||
@@ -49,7 +49,7 @@ DO NOT use deprecated PrimeVue components. Use these replacements instead:
|
||||
|
||||
## Development Guidelines
|
||||
1. Leverage VueUse functions for performance-enhancing styles
|
||||
2. Use lodash for utility functions
|
||||
2. Use es-toolkit for utility functions
|
||||
3. Use TypeScript for type safety
|
||||
4. Implement proper props and emits definitions
|
||||
5. Utilize Vue 3's Teleport component when needed
|
||||
|
||||
2
.github/copilot-instructions.md
vendored
@@ -18,7 +18,7 @@ Use Tailwind CSS for styling
|
||||
|
||||
Leverage VueUse functions for performance-enhancing styles
|
||||
|
||||
Use lodash for utility functions
|
||||
Use es-toolkit for utility functions
|
||||
|
||||
Use TypeScript for type safety
|
||||
|
||||
|
||||
20
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
## Summary
|
||||
|
||||
<!-- One sentence describing what changed and why. -->
|
||||
|
||||
## Changes
|
||||
|
||||
- **What**: <!-- Core functionality added/modified -->
|
||||
- **Breaking**: <!-- Any breaking changes (if none, remove this line) -->
|
||||
- **Dependencies**: <!-- New dependencies (if none, remove this line) -->
|
||||
|
||||
## Review Focus
|
||||
|
||||
<!-- Critical design decisions or edge cases that need attention -->
|
||||
|
||||
<!-- If this PR fixes an issue, uncomment and update the line below -->
|
||||
<!-- Fixes #ISSUE_NUMBER -->
|
||||
|
||||
## Screenshots (if applicable)
|
||||
|
||||
<!-- Add screenshots or video recording to help explain your changes -->
|
||||
1
.github/workflows/lint-and-format.yaml
vendored
@@ -56,6 +56,7 @@ jobs:
|
||||
run: |
|
||||
npm run lint
|
||||
npm run format:check
|
||||
npm run knip
|
||||
|
||||
- name: Comment on PR about auto-fix
|
||||
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
5
.github/workflows/update-manager-types.yaml
vendored
@@ -61,6 +61,11 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Lint generated types
|
||||
run: |
|
||||
echo "Linting generated ComfyUI-Manager API types..."
|
||||
npm run lint:fix:no-cache -- ./src/types/generatedManagerTypes.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
|
||||
5
.github/workflows/update-registry-types.yaml
vendored
@@ -61,6 +61,11 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Lint generated types
|
||||
run: |
|
||||
echo "Linting generated Comfy Registry API types..."
|
||||
npm run lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
|
||||
15
.gitignore
vendored
@@ -7,6 +7,15 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Package manager lockfiles (allow users to use different package managers)
|
||||
bun.lock
|
||||
bun.lockb
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
@@ -41,7 +50,6 @@ tests-ui/workflows/examples
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
browser_tests/**/*-win32.png
|
||||
browser_tests/**/*-darwin.png
|
||||
|
||||
.env
|
||||
|
||||
@@ -59,5 +67,8 @@ dist.zip
|
||||
# Temporary repository directory
|
||||
templates_repo/
|
||||
|
||||
# Vite’s timestamped config modules
|
||||
# Vite's timestamped config modules
|
||||
vite.config.mts.timestamp-*.mjs
|
||||
|
||||
# Linux core dumps
|
||||
./core
|
||||
|
||||
@@ -9,10 +9,14 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'],
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
Note: For Traditional Chinese (Taiwan), use Taiwan-specific terminology and traditional characters.
|
||||
|
||||
IMPORTANT Chinese Translation Guidelines:
|
||||
- For 'zh' locale: Use ONLY Simplified Chinese characters (简体中文). Common examples: 节点 (not 節點), 画布 (not 畫布), 图像 (not 圖像), 选择 (not 選擇), 减小 (not 減小).
|
||||
- For 'zh-TW' locale: Use ONLY Traditional Chinese characters (繁體中文) with Taiwan-specific terminology.
|
||||
- NEVER mix Simplified and Traditional Chinese characters within the same locale.
|
||||
`
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ The development of successive minor versions overlaps. For example, while versio
|
||||
<summary>v1.5: Native translation (i18n)</summary>
|
||||
|
||||
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
|
||||
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
|
||||
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, Korean, or Arabic. This native
|
||||
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
|
||||
|
||||
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"id": "dec788c2-9829-4a5d-a1ee-d6f0a678b42a",
|
||||
"revision": 0,
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [413, 389],
|
||||
"size": [425.27801513671875, 180.6060791015625],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [6]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": ["text, watermark"]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [415, 186],
|
||||
"size": [422.84503173828125, 164.31304931640625],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [4]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [473, 609],
|
||||
"size": [315, 106],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [2]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [512, 512, 1]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [863, 186],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 6
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [7]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1209, 188],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 7
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"slot_index": 0,
|
||||
"links": [9]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveImage",
|
||||
"pos": [1451, 189],
|
||||
"size": [210, 58],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [26, 474],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": [1]
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": [3, 5]
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [8]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 4, 0, 3, 0, "MODEL"],
|
||||
[2, 5, 0, 3, 3, "LATENT"],
|
||||
[3, 4, 1, 6, 0, "CLIP"],
|
||||
[4, 6, 0, 3, 1, "CONDITIONING"],
|
||||
[5, 4, 1, 7, 0, "CLIP"],
|
||||
[6, 7, 0, 3, 2, "CONDITIONING"],
|
||||
[7, 3, 0, 8, 0, "LATENT"],
|
||||
[8, 4, 2, 8, 1, "VAE"],
|
||||
[9, 8, 0, 9, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
"pos": [372.66668701171875, 415.33331298828125],
|
||||
"linkIds": [3]
|
||||
}
|
||||
],
|
||||
"linkExtensions": [
|
||||
{
|
||||
"id": 3,
|
||||
"parentId": 1
|
||||
}
|
||||
],
|
||||
"frontendVersion": "1.26.1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export class Topbar {
|
||||
workflowName: string,
|
||||
command: 'Save' | 'Save As' | 'Export'
|
||||
) {
|
||||
await this.triggerTopbarCommand(['Workflow', command])
|
||||
await this.triggerTopbarCommand(['File', command])
|
||||
await this.getSaveDialog().fill(workflowName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
|
||||
@@ -72,8 +72,8 @@ export class Topbar {
|
||||
}
|
||||
|
||||
async triggerTopbarCommand(path: string[]) {
|
||||
if (path.length < 2) {
|
||||
throw new Error('Path is too short')
|
||||
if (path.length < 1) {
|
||||
throw new Error('Path cannot be empty')
|
||||
}
|
||||
|
||||
const menu = await this.openTopbarMenu()
|
||||
@@ -85,6 +85,13 @@ export class Topbar {
|
||||
.locator('.p-tieredmenu-item')
|
||||
.filter({ has: topLevelMenuItem })
|
||||
await topLevelMenu.waitFor({ state: 'visible' })
|
||||
|
||||
// Handle top-level commands (like "New")
|
||||
if (path.length === 1) {
|
||||
await topLevelMenuItem.click()
|
||||
return
|
||||
}
|
||||
|
||||
await topLevelMenu.hover()
|
||||
|
||||
let currentMenu = topLevelMenu
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import fs from 'fs'
|
||||
import _ from 'lodash'
|
||||
import path from 'path'
|
||||
import type { Request, Route } from 'playwright'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@@ -75,7 +75,9 @@ export default class TaskHistory {
|
||||
|
||||
private async handleGetView(route: Route) {
|
||||
const fileName = getFilenameParam(route.request())
|
||||
if (!this.outputContentTypes.has(fileName)) route.continue()
|
||||
if (!this.outputContentTypes.has(fileName)) {
|
||||
return route.continue()
|
||||
}
|
||||
|
||||
const asset = this.loadAsset(fileName)
|
||||
return route.fulfill({
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Command search box', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
})
|
||||
|
||||
test('Can trigger command mode with ">" prefix', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
|
||||
// Type ">" to enter command mode
|
||||
await comfyPage.searchBox.input.fill('>')
|
||||
|
||||
// Verify filter button is hidden in command mode
|
||||
const filterButton = comfyPage.page.locator('.filter-button')
|
||||
await expect(filterButton).not.toBeVisible()
|
||||
|
||||
// Verify placeholder text changes
|
||||
await expect(comfyPage.searchBox.input).toHaveAttribute(
|
||||
'placeholder',
|
||||
expect.stringContaining('Search Commands')
|
||||
)
|
||||
})
|
||||
|
||||
test('Shows command list when entering command mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.input.fill('>')
|
||||
|
||||
// Wait for dropdown to appear
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
|
||||
// Check that commands are shown
|
||||
const firstItem = comfyPage.searchBox.dropdown.locator('li').first()
|
||||
await expect(firstItem).toBeVisible()
|
||||
|
||||
// Verify it shows a command item with icon
|
||||
const commandIcon = firstItem.locator('.item-icon')
|
||||
await expect(commandIcon).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can search and filter commands', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.input.fill('>save')
|
||||
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
await comfyPage.page.waitForTimeout(500) // Wait for search to complete
|
||||
|
||||
// Get all visible command items
|
||||
const items = comfyPage.searchBox.dropdown.locator('li')
|
||||
const count = await items.count()
|
||||
|
||||
// Should have filtered results
|
||||
expect(count).toBeGreaterThan(0)
|
||||
expect(count).toBeLessThan(10) // Should be filtered, not showing all
|
||||
|
||||
// Verify first result contains "save"
|
||||
const firstLabel = await items.first().locator('.item-label').textContent()
|
||||
expect(firstLabel?.toLowerCase()).toContain('save')
|
||||
})
|
||||
|
||||
test('Shows keybindings for commands that have them', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.input.fill('>undo')
|
||||
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Find the undo command
|
||||
const undoItem = comfyPage.searchBox.dropdown
|
||||
.locator('li')
|
||||
.filter({ hasText: 'Undo' })
|
||||
.first()
|
||||
|
||||
// Check if keybinding is shown (if configured)
|
||||
const keybinding = undoItem.locator('.item-keybinding')
|
||||
const keybindingCount = await keybinding.count()
|
||||
|
||||
// Keybinding might or might not be present depending on configuration
|
||||
if (keybindingCount > 0) {
|
||||
await expect(keybinding).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('Executes command on selection', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.input.fill('>new blank')
|
||||
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Count nodes before
|
||||
const nodesBefore = await comfyPage.page
|
||||
.locator('.litegraph.litenode')
|
||||
.count()
|
||||
|
||||
// Select the new blank workflow command
|
||||
const newBlankItem = comfyPage.searchBox.dropdown
|
||||
.locator('li')
|
||||
.filter({ hasText: 'New Blank Workflow' })
|
||||
.first()
|
||||
await newBlankItem.click()
|
||||
|
||||
// Search box should close
|
||||
await expect(comfyPage.searchBox.input).not.toBeVisible()
|
||||
|
||||
// Verify workflow was cleared (no nodes)
|
||||
const nodesAfter = await comfyPage.page
|
||||
.locator('.litegraph.litenode')
|
||||
.count()
|
||||
expect(nodesAfter).toBe(0)
|
||||
})
|
||||
|
||||
test('Returns to node search when removing ">"', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
|
||||
// Enter command mode
|
||||
await comfyPage.searchBox.input.fill('>')
|
||||
await expect(comfyPage.page.locator('.filter-button')).not.toBeVisible()
|
||||
|
||||
// Return to node search by filling with empty string to trigger search
|
||||
await comfyPage.searchBox.input.fill('')
|
||||
|
||||
// Small wait for UI update
|
||||
await comfyPage.page.waitForTimeout(200)
|
||||
|
||||
// Filter button should be visible again
|
||||
await expect(comfyPage.page.locator('.filter-button')).toBeVisible()
|
||||
|
||||
// Placeholder should change back
|
||||
await expect(comfyPage.searchBox.input).toHaveAttribute(
|
||||
'placeholder',
|
||||
expect.stringContaining('Search Nodes')
|
||||
)
|
||||
})
|
||||
|
||||
test('Command search is case insensitive', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
|
||||
// Search with lowercase
|
||||
await comfyPage.searchBox.input.fill('>SAVE')
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Should find save commands
|
||||
const items = comfyPage.searchBox.dropdown.locator('li')
|
||||
const count = await items.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
|
||||
// Verify it found save-related commands
|
||||
const firstLabel = await items.first().locator('.item-label').textContent()
|
||||
expect(firstLabel?.toLowerCase()).toContain('save')
|
||||
})
|
||||
})
|
||||
@@ -17,11 +17,11 @@ test.describe('Group Node', () => {
|
||||
await libraryTab.open()
|
||||
})
|
||||
|
||||
test.skip('Is added to node library sidebar', async ({ comfyPage }) => {
|
||||
test('Is added to node library sidebar', async ({ comfyPage }) => {
|
||||
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
|
||||
})
|
||||
|
||||
test.skip('Can be added to canvas using node library sidebar', async ({
|
||||
test('Can be added to canvas using node library sidebar', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialNodeCount = await comfyPage.getGraphNodesCount()
|
||||
@@ -34,7 +34,7 @@ test.describe('Group Node', () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
|
||||
})
|
||||
|
||||
test.skip('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
|
||||
test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
|
||||
await libraryTab.getFolder(groupNodeCategory).click()
|
||||
await libraryTab
|
||||
.getNode(groupNodeName)
|
||||
@@ -61,7 +61,7 @@ test.describe('Group Node', () => {
|
||||
).toHaveLength(0)
|
||||
})
|
||||
|
||||
test.skip('Displays preview on bookmark hover', async ({ comfyPage }) => {
|
||||
test('Displays preview on bookmark hover', async ({ comfyPage }) => {
|
||||
await libraryTab.getFolder(groupNodeCategory).click()
|
||||
await libraryTab
|
||||
.getNode(groupNodeName)
|
||||
@@ -95,7 +95,7 @@ test.describe('Group Node', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test.skip('Displays tooltip on title hover', async ({ comfyPage }) => {
|
||||
test('Displays tooltip on title hover', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||
await comfyPage.convertAllNodesToGroupNode('Group Node')
|
||||
await comfyPage.page.mouse.move(47, 173)
|
||||
@@ -104,7 +104,7 @@ test.describe('Group Node', () => {
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
||||
})
|
||||
|
||||
test.skip('Manage group opens with the correct group selected', async ({
|
||||
test('Manage group opens with the correct group selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const makeGroup = async (name, type1, type2) => {
|
||||
@@ -165,7 +165,7 @@ test.describe('Group Node', () => {
|
||||
expect(visibleInputCount).toBe(2)
|
||||
})
|
||||
|
||||
test.skip('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
test('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const expectSingleNode = async (type: string) => {
|
||||
@@ -268,10 +268,7 @@ test.describe('Group Node', () => {
|
||||
await comfyPage.setSetting('Comfy.ConfirmClear', false)
|
||||
|
||||
// Clear workflow
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand([
|
||||
'Edit',
|
||||
'Clear Workflow'
|
||||
])
|
||||
await comfyPage.executeCommand('Comfy.ClearWorkflow')
|
||||
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
@@ -280,7 +277,7 @@ test.describe('Group Node', () => {
|
||||
test('Copies and pastes group node into a newly created blank workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
@@ -296,7 +293,7 @@ test.describe('Group Node', () => {
|
||||
test('Serializes group node after copy and paste across workflows', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.ctrlV()
|
||||
const currentGraphState = await comfyPage.page.evaluate(() =>
|
||||
window['app'].graph.serialize()
|
||||
|
||||
@@ -684,7 +684,7 @@ test.describe('Load workflow', () => {
|
||||
workflowA = generateUniqueFilename()
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowA)
|
||||
workflowB = generateUniqueFilename()
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowB)
|
||||
|
||||
// Wait for localStorage to persist the workflow paths before reloading
|
||||
|
||||
@@ -75,7 +75,7 @@ test.describe('Menu', () => {
|
||||
|
||||
test('Displays keybinding next to item', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.openTopbarMenu()
|
||||
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('Workflow')
|
||||
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('File')
|
||||
await workflowMenuItem.hover()
|
||||
const exportTag = comfyPage.page.locator('.keybinding-tag', {
|
||||
hasText: 'Ctrl + s'
|
||||
|
||||
@@ -24,8 +24,14 @@ test.describe('Minimap', () => {
|
||||
const minimapViewport = minimapContainer.locator('.minimap-viewport')
|
||||
await expect(minimapViewport).toBeVisible()
|
||||
|
||||
await expect(minimapContainer).toHaveCSS('position', 'absolute')
|
||||
await expect(minimapContainer).toHaveCSS('z-index', '1000')
|
||||
await expect(minimapContainer).toHaveCSS('position', 'relative')
|
||||
|
||||
// position and z-index validation moved to the parent container of the minimap
|
||||
const minimapMainContainer = comfyPage.page.locator(
|
||||
'.minimap-main-container'
|
||||
)
|
||||
await expect(minimapMainContainer).toHaveCSS('position', 'absolute')
|
||||
await expect(minimapMainContainer).toHaveCSS('z-index', '1000')
|
||||
})
|
||||
|
||||
test('Validate minimap toggle button state', async ({ comfyPage }) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('Reroute Node', () => {
|
||||
[workflowName]: workflowName
|
||||
})
|
||||
await comfyPage.setup()
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
|
||||
// Insert the workflow
|
||||
const workflowsTab = comfyPage.menu.workflowsTab
|
||||
@@ -100,4 +100,29 @@ test.describe('LiteGraph Native Reroute Node', () => {
|
||||
'native_reroute_context_menu.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can delete link that is connected to two reroutes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
|
||||
await comfyPage.loadWorkflow(
|
||||
'reroute/single-native-reroute-default-workflow'
|
||||
)
|
||||
|
||||
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
|
||||
// since the link is a bezier curve and not a straight line.
|
||||
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
|
||||
|
||||
// Click the middle point of the link to open the context menu.
|
||||
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
|
||||
|
||||
// Click the "Delete" context menu option.
|
||||
await comfyPage.page
|
||||
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
|
||||
.click()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'native_reroute_delete_from_midpoint_context_menu.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 92 KiB |
@@ -24,11 +24,11 @@ test.describe('Canvas Right Click Menu', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
|
||||
})
|
||||
|
||||
test.skip('Can convert to group node', async ({ comfyPage }) => {
|
||||
test('Can convert to group node', async ({ comfyPage }) => {
|
||||
await comfyPage.select2Nodes()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node')
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
|
||||
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 98 KiB |
@@ -317,6 +317,25 @@ test.describe('Workflows sidebar', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test('Can duplicate workflow from context menu', async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
'workflow1.json': 'default.json'
|
||||
})
|
||||
|
||||
const { workflowsTab } = comfyPage.menu
|
||||
await workflowsTab.open()
|
||||
|
||||
await workflowsTab
|
||||
.getPersistedItem('workflow1.json')
|
||||
.click({ button: 'right' })
|
||||
await comfyPage.clickContextMenuItem('Duplicate')
|
||||
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'*workflow1 (Copy).json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
'workflow1.json': 'default.json'
|
||||
|
||||
@@ -704,4 +704,103 @@ test.describe('Subgraph Operations', () => {
|
||||
expect(finalCount).toBe(parentCount)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Navigation Hotkeys', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('Navigation hotkey can be customized', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Change the Exit Subgraph keybinding from Escape to Alt+Q
|
||||
await comfyPage.setSetting('Comfy.Keybinding.NewBindings', [
|
||||
{
|
||||
commandId: 'Comfy.Graph.ExitSubgraph',
|
||||
combo: {
|
||||
key: 'q',
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
shift: false
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
await comfyPage.setSetting('Comfy.Keybinding.UnsetBindings', [
|
||||
{
|
||||
commandId: 'Comfy.Graph.ExitSubgraph',
|
||||
combo: {
|
||||
key: 'Escape',
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Reload the page
|
||||
await comfyPage.page.reload()
|
||||
await comfyPage.page.waitForTimeout(1024)
|
||||
|
||||
// Navigate into subgraph
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
|
||||
|
||||
// Verify we're in a subgraph
|
||||
expect(await isInSubgraph(comfyPage)).toBe(true)
|
||||
|
||||
// Test that Escape no longer exits subgraph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
if (!(await isInSubgraph(comfyPage))) {
|
||||
throw new Error('Not in subgraph')
|
||||
}
|
||||
|
||||
// Test that Alt+Q now exits subgraph
|
||||
await comfyPage.page.keyboard.press('Alt+q')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await isInSubgraph(comfyPage)).toBe(false)
|
||||
})
|
||||
|
||||
test('Escape prioritizes closing dialogs over exiting subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
|
||||
|
||||
// Verify we're in a subgraph
|
||||
if (!(await isInSubgraph(comfyPage))) {
|
||||
throw new Error('Not in subgraph')
|
||||
}
|
||||
|
||||
// Open settings dialog using hotkey
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
await comfyPage.page.waitForSelector('.settings-container', {
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
// Press Escape - should close dialog, not exit subgraph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Dialog should be closed
|
||||
await expect(
|
||||
comfyPage.page.locator('.settings-container')
|
||||
).not.toBeVisible()
|
||||
|
||||
// Should still be in subgraph
|
||||
expect(await isInSubgraph(comfyPage)).toBe(true)
|
||||
|
||||
// Press Escape again - now should exit subgraph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await isInSubgraph(comfyPage)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -63,7 +63,7 @@ test.describe('Workflow Tab Thumbnails', () => {
|
||||
test('Should show thumbnail when hovering over a non-active tab', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
const thumbnailImg = await getTabThumbnailImage(
|
||||
comfyPage,
|
||||
0,
|
||||
@@ -73,7 +73,7 @@ test.describe('Workflow Tab Thumbnails', () => {
|
||||
})
|
||||
|
||||
test('Should not show thumbnail for active tab', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
const thumbnailImg = await getTabThumbnailImage(
|
||||
comfyPage,
|
||||
1,
|
||||
@@ -105,7 +105,7 @@ test.describe('Workflow Tab Thumbnails', () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Create a new workflow (tab 1) which will be empty
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Now we have two tabs: tab 0 (default workflow with nodes) and tab 1 (empty)
|
||||
|
||||
@@ -14,7 +14,10 @@ export default [
|
||||
ignores: [
|
||||
'src/scripts/*',
|
||||
'src/extensions/core/*',
|
||||
'src/types/vue-shim.d.ts'
|
||||
'src/types/vue-shim.d.ts',
|
||||
// Generated files that don't need linting
|
||||
'src/types/comfyRegistryTypes.ts',
|
||||
'src/types/generatedManagerTypes.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
75
knip.config.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { KnipConfig } from 'knip'
|
||||
|
||||
const config: KnipConfig = {
|
||||
entry: [
|
||||
'src/main.ts',
|
||||
'vite.config.mts',
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
'eslint.config.js',
|
||||
'tailwind.config.js',
|
||||
'postcss.config.js',
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts',
|
||||
'vitest.config.ts',
|
||||
'scripts/**/*.{js,ts}'
|
||||
],
|
||||
project: [
|
||||
'src/**/*.{js,ts,vue}',
|
||||
'tests-ui/**/*.{js,ts,vue}',
|
||||
'browser_tests/**/*.{js,ts}',
|
||||
'scripts/**/*.{js,ts}'
|
||||
],
|
||||
ignore: [
|
||||
// Generated files
|
||||
'dist/**',
|
||||
'types/**',
|
||||
'node_modules/**',
|
||||
// Config files that might not show direct usage
|
||||
'.husky/**',
|
||||
// Temporary or cache files
|
||||
'.vite/**',
|
||||
'coverage/**',
|
||||
// i18n config
|
||||
'.i18nrc.cjs',
|
||||
// Test setup files
|
||||
'browser_tests/globalSetup.ts',
|
||||
'browser_tests/globalTeardown.ts',
|
||||
'browser_tests/utils/**',
|
||||
// Scripts
|
||||
'scripts/**',
|
||||
// Vite config files
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
// Auto generated manager types
|
||||
'src/types/generatedManagerTypes.ts'
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
// Vue-specific configuration
|
||||
vue: true,
|
||||
// Only check for unused files, disable all other rules
|
||||
// TODO: Gradually enable other rules - see https://github.com/Comfy-Org/ComfyUI_frontend/issues/4888
|
||||
rules: {
|
||||
binaries: 'off',
|
||||
classMembers: 'off',
|
||||
dependencies: 'off',
|
||||
devDependencies: 'off',
|
||||
duplicates: 'off',
|
||||
enumMembers: 'off',
|
||||
exports: 'off',
|
||||
nsExports: 'off',
|
||||
nsTypes: 'off',
|
||||
types: 'off',
|
||||
unlisted: 'off'
|
||||
},
|
||||
// Include dependencies analysis
|
||||
includeEntryExports: true,
|
||||
// Workspace configuration for monorepo-like structure
|
||||
workspaces: {
|
||||
'.': {
|
||||
entry: ['src/main.ts']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default config
|
||||
565
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.1",
|
||||
"version": "1.26.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.1",
|
||||
"version": "1.26.4",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -30,12 +30,12 @@
|
||||
"axios": "^1.8.2",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.2",
|
||||
"marked": "^15.0.11",
|
||||
"pinia": "^2.1.7",
|
||||
@@ -62,7 +62,6 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/three": "^0.169.0",
|
||||
@@ -80,6 +79,7 @@
|
||||
"happy-dom": "^15.11.0",
|
||||
"husky": "^9.0.11",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"knip": "^5.62.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
@@ -1001,6 +1001,40 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
|
||||
"integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.4",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
|
||||
"integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz",
|
||||
"integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -3069,6 +3103,19 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz",
|
||||
"integrity": "sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.5",
|
||||
"@emnapi/runtime": "^1.4.5",
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -3120,6 +3167,275 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-android-arm-eabi": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.6.1.tgz",
|
||||
"integrity": "sha512-Ma/kg29QJX1Jzelv0Q/j2iFuUad1WnjgPjpThvjqPjpOyLjCUaiFCCnshhmWjyS51Ki1Iol3fjf1qAzObf8GIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-android-arm64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.6.1.tgz",
|
||||
"integrity": "sha512-xjL/FKKc5p8JkFWiH7pJWSzsewif3fRf1rw2qiRxRvq1uIa6l7Zoa14Zq2TNWEsqDjdeOrlJtfWiPNRnevK0oQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-darwin-arm64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.6.1.tgz",
|
||||
"integrity": "sha512-u0yrJ3NHE0zyCjiYpIyz4Vmov21MA0yFKbhHgixDU/G6R6nvC8ZpuSFql3+7C8ttAK9p8WpqOGweepfcilH5Bw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-darwin-x64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.6.1.tgz",
|
||||
"integrity": "sha512-2lox165h1EhzxcC8edUy0znXC/hnAbUPaMpYKVlzLpB2AoYmgU4/pmofFApj+axm2FXpNamjcppld8EoHo06rw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-freebsd-x64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.6.1.tgz",
|
||||
"integrity": "sha512-F45MhEQ7QbHfsvZtVNuA/9obu3il7QhpXYmCMfxn7Zt9nfAOw4pQ8hlS5DroHVp3rW35u9F7x0sixk/QEAi3qQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.6.1.tgz",
|
||||
"integrity": "sha512-r+3+MTTl0tD4NoWbfTIItAxJvuyIU7V0fwPDXrv7Uj64vZ3OYaiyV+lVaeU89Bk/FUUQxeUpWBwdKNKHjyRNQw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm-musleabihf": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.6.1.tgz",
|
||||
"integrity": "sha512-TBTZ63otsWZ72Z8ZNK2JVS0HW1w9zgOixJTFDNrYPUUW1pXGa28KAjQ1yGawj242WLAdu3lwdNIWtkxeO2BLxQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-SjwhNynjSG2yMdyA0f7wz7Yvo3ppejO+ET7n2oiI7ApCXrwxMzeRWjBzQt+oVWr2HzVOfaEcDS9rMtnR83ulig==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm64-musl": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.6.1.tgz",
|
||||
"integrity": "sha512-f4EMidK6rosInBzPMnJ0Ri4RttFCvvLNUNDFUBtELW/MFkBwPTDlvbsmW0u0Mk/ruBQ2WmRfOZ6tT62kWMcX2Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-ppc64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-1umENVKeUsrWnf5IlF/6SM7DCv8G6CoKI2LnYR6qhZuLYDPS4PBZ0Jow3UDV9Rtbv5KRPcA3/uXjI88ntWIcOQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-riscv64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-Hjyp1FRdJhsEpIxsZq5VcDuFc8abC0Bgy8DWEa31trCKoTz7JqA7x3E2dkFbrAKsEFmZZ0NvuG5Ip3oIRARhow==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-riscv64-musl": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.6.1.tgz",
|
||||
"integrity": "sha512-ODJOJng6f3QxpAXhLel3kyWs8rPsJeo9XIZHzA7p//e+5kLMDU7bTVk4eZnUHuxsqsB8MEvPCicJkKCEuur5Ag==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-s390x-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-hCzRiLhqe1ZOpHTsTGKp7gnMJRORlbCthawBueer2u22RVAka74pV/+4pP1tqM07mSlQn7VATuWaDw9gCl+cVg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-x64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-JansPD8ftOzMYIC3NfXJ68tt63LEcIAx44Blx6BAd7eY880KX7A0KN3hluCrelCz5aQkPaD95g8HBiJmKaEi2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-x64-musl": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.6.1.tgz",
|
||||
"integrity": "sha512-R78ES1rd4z2x5NrFPtSWb/ViR1B8wdl+QN2X8DdtoYcqZE/4tvWtn9ZTCXMEzUp23tchJ2wUB+p6hXoonkyLpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-wasm32-wasi": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.6.1.tgz",
|
||||
"integrity": "sha512-qAR3tYIf3afkij/XYunZtlz3OH2Y4ni10etmCFIJB5VRGsqJyI6Hl+2dXHHGJNwbwjXjSEH/KWJBpVroF3TxBw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-win32-arm64-msvc": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.6.1.tgz",
|
||||
"integrity": "sha512-QqygWygIuemGkaBA48POOTeinbVvlamqh6ucm8arGDGz/mB5O00gXWxed12/uVrYEjeqbMkla/CuL3fjL3EKvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-win32-ia32-msvc": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.6.1.tgz",
|
||||
"integrity": "sha512-N2+kkWwt/bk0JTCxhPuK8t8JMp3nd0n2OhwOkU8KO4a7roAJEa4K1SZVjMv5CqUIr5sx2CxtXRBoFDiORX5oBg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-win32-x64-msvc": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.6.1.tgz",
|
||||
"integrity": "sha512-DfMg3cU9bJUbN62Prbp4fGCtLgexuwyEaQGtZAp8xmi1Ii26uflOGx0FJkFTF6lVMSFoIRFvIL8gsw5/ZdHrMw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@pinia/testing": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.5.tgz",
|
||||
@@ -4486,6 +4802,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
||||
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/argparse": {
|
||||
"version": "1.0.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz",
|
||||
@@ -4559,12 +4886,6 @@
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz",
|
||||
"integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
@@ -7670,6 +7991,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.39.9",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.9.tgz",
|
||||
"integrity": "sha512-9OtbkZmTA2Qc9groyA1PUNeb6knVTkvB2RSdr/LcJXDL8IdEakaxwXLHXa7VX/Wj0GmdMJPR3WhnPGhiP3E+qg==",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
@@ -8469,16 +8799,17 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.4"
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
@@ -8551,6 +8882,16 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-package-json": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz",
|
||||
"integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"walk-up-path": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
@@ -8846,6 +9187,22 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/formatly": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/formatly/-/formatly-0.2.4.tgz",
|
||||
"integrity": "sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fd-package-json": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"formatly": "bin/index.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
@@ -10517,6 +10874,109 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.62.0.tgz",
|
||||
"integrity": "sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/webpro"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/knip"
|
||||
},
|
||||
{
|
||||
"type": "polar",
|
||||
"url": "https://polar.sh/webpro-nl"
|
||||
}
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"fast-glob": "^3.3.3",
|
||||
"formatly": "^0.2.4",
|
||||
"jiti": "^2.4.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"oxc-resolver": "^11.1.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.1",
|
||||
"smol-toml": "^1.3.4",
|
||||
"strip-json-comments": "5.0.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"knip": "bin/knip.js",
|
||||
"knip-bun": "bin/knip-bun.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18",
|
||||
"typescript": ">=5.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/knip/node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/strip-json-comments": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.2.tgz",
|
||||
"integrity": "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/kolorist": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
||||
@@ -10992,7 +11452,8 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
@@ -12388,6 +12849,22 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz",
|
||||
"integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"napi-postinstall": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/napi-postinstall"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -12741,6 +13218,41 @@
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oxc-resolver": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.6.1.tgz",
|
||||
"integrity": "sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxc-resolver/binding-android-arm-eabi": "11.6.1",
|
||||
"@oxc-resolver/binding-android-arm64": "11.6.1",
|
||||
"@oxc-resolver/binding-darwin-arm64": "11.6.1",
|
||||
"@oxc-resolver/binding-darwin-x64": "11.6.1",
|
||||
"@oxc-resolver/binding-freebsd-x64": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm-gnueabihf": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm-musleabihf": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm64-musl": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-ppc64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-riscv64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-riscv64-musl": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-s390x-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-x64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-x64-musl": "11.6.1",
|
||||
"@oxc-resolver/binding-wasm32-wasi": "11.6.1",
|
||||
"@oxc-resolver/binding-win32-arm64-msvc": "11.6.1",
|
||||
"@oxc-resolver/binding-win32-ia32-msvc": "11.6.1",
|
||||
"@oxc-resolver/binding-win32-x64-msvc": "11.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
@@ -14804,6 +15316,19 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz",
|
||||
"integrity": "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/cyyynthia"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -17400,6 +17925,16 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/walk-up-path": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz",
|
||||
"integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
|
||||
13
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.1",
|
||||
"version": "1.26.4",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -21,8 +21,11 @@
|
||||
"test:component": "vitest run src/components/",
|
||||
"prepare": "husky || true",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint": "eslint src --cache",
|
||||
"lint:fix": "eslint src --cache --fix",
|
||||
"lint:no-cache": "eslint src",
|
||||
"lint:fix:no-cache": "eslint src --fix",
|
||||
"knip": "knip",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts"
|
||||
@@ -38,7 +41,6 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/three": "^0.169.0",
|
||||
@@ -56,6 +58,7 @@
|
||||
"happy-dom": "^15.11.0",
|
||||
"husky": "^9.0.11",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"knip": "^5.62.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
@@ -96,12 +99,12 @@
|
||||
"axios": "^1.8.2",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.2",
|
||||
"marked": "^15.0.11",
|
||||
"pinia": "^2.1.7",
|
||||
|
||||
@@ -51,7 +51,7 @@ const template = await fetch('/templates/default.json')
|
||||
|
||||
## General Guidelines
|
||||
|
||||
- Use lodash for utility functions
|
||||
- Use es-toolkit for utility functions
|
||||
- Implement proper TypeScript types
|
||||
- Follow Vue 3 composition API style guide
|
||||
- Use vue-i18n for ALL user-facing strings in `src/locales/en/main.json`
|
||||
|
||||
@@ -616,7 +616,8 @@ audio.comfy-audio.empty-audio-widget {
|
||||
.comfy-load-3d canvas,
|
||||
.comfy-load-3d-animation canvas,
|
||||
.comfy-preview-3d canvas,
|
||||
.comfy-preview-3d-animation canvas{
|
||||
.comfy-preview-3d-animation canvas,
|
||||
.comfy-load-3d-viewer canvas{
|
||||
display: flex;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
|
||||
6
src/assets/icons/custom/ai-model.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.91396 12.7428L5.41396 10.7428C5.57175 10.1116 5.09439 9.50024 4.44382 9.50024H2.50538C2.04651 9.50024 1.64652 9.81253 1.53523 10.2577L1.03523 12.2577C0.877446 12.8888 1.3548 13.5002 2.00538 13.5002H3.94382C4.40269 13.5002 4.80267 13.1879 4.91396 12.7428Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M5.91396 6.74277L6.41396 4.74277C6.57175 4.11163 6.09439 3.50024 5.44382 3.50024H3.50538C3.04651 3.50024 2.64652 3.81253 2.53523 4.2577L2.03523 6.2577C1.87745 6.88885 2.3548 7.50024 3.00538 7.50024H4.94382C5.40269 7.50024 5.80267 7.18794 5.91396 6.74277Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M10.914 12.7428L11.414 10.7428C11.5718 10.1116 11.0944 9.50024 10.4438 9.50024H8.50538C8.04651 9.50024 7.64652 9.81253 7.53523 10.2577L7.03523 12.2577C6.87745 12.8888 7.3548 13.5002 8.00538 13.5002H9.94382C10.4027 13.5002 10.8027 13.1879 10.914 12.7428Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M12.2342 5.46739L11.5287 7.11354C11.4248 7.35597 11.0811 7.35597 10.9772 7.11354L10.2717 5.46739C10.2414 5.39659 10.185 5.34017 10.1141 5.30983L8.468 4.60433C8.22557 4.50044 8.22557 4.15675 8.468 4.05285L10.1141 3.34736C10.185 3.31701 10.2414 3.26059 10.2717 3.18979L10.9772 1.54364C11.0811 1.30121 11.4248 1.30121 11.5287 1.54364L12.2342 3.18979C12.2645 3.26059 12.3209 3.31701 12.3918 3.34736L14.0379 4.05285C14.2803 4.15675 14.2803 4.50044 14.0379 4.60433L12.3918 5.30983C12.3209 5.34017 12.2645 5.39659 12.2342 5.46739Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
src/assets/icons/custom/node.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.6667 10L10.598 10.2577C10.4812 10.6954 10.0848 11 9.63172 11H5.30161C4.64458 11 4.16608 10.3772 4.33538 9.74234L5.40204 5.74234C5.51878 5.30458 5.91523 5 6.36828 5H10.8286C11.4199 5 11.8505 5.56051 11.6982 6.13185L11.6736 6.22389M14 8H10M4.5 8H2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 405 B |
5
src/assets/icons/custom/template.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.1894 6.24254L13.6894 4.24254C13.8471 3.61139 13.3698 3 12.7192 3H3.78077C3.3219 3 2.92192 3.3123 2.81062 3.75746L2.31062 5.75746C2.15284 6.38861 2.63019 7 3.28077 7H12.2192C12.6781 7 13.0781 6.6877 13.1894 6.24254Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M13.1894 12.2425L13.6894 10.2425C13.8471 9.61139 13.3698 9 12.7192 9H8.78077C8.3219 9 7.92192 9.3123 7.81062 9.75746L7.31062 11.7575C7.15284 12.3886 7.6302 13 8.28077 13H12.2192C12.6781 13 13.0781 12.6877 13.1894 12.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M5.18936 12.2425L5.68936 10.2425C5.84714 9.61139 5.36978 9 4.71921 9H3.78077C3.3219 9 2.92192 9.3123 2.81062 9.75746L2.31062 11.7575C2.15284 12.3886 2.6302 13 3.28077 13H4.21921C4.67808 13 5.07806 12.6877 5.18936 12.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 970 B |
@@ -20,7 +20,7 @@ import {
|
||||
useLocalStorage,
|
||||
watchDebounced
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import Panel from 'primevue/panel'
|
||||
import { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
<div class="shortcut-info flex-grow pr-4">
|
||||
<div class="shortcut-name text-sm font-medium">
|
||||
{{ command.getTranslatedLabel() }}
|
||||
{{ t(`commands.${normalizeI18nKey(command.id)}.label`) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +50,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Breadcrumb from 'primevue/breadcrumb'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { computed, onUpdated, ref, watch } from 'vue'
|
||||
@@ -98,18 +97,6 @@ const home = computed(() => ({
|
||||
}
|
||||
}))
|
||||
|
||||
// Escape exits from the current subgraph.
|
||||
useEventListener(document, 'keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||
|
||||
canvas.setGraph(
|
||||
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Check for overflow on breadcrumb items and collapse/expand the breadcrumb to fit
|
||||
let overflowObserver: ReturnType<typeof useOverflowObserver> | undefined
|
||||
watch(breadcrumbElement, (el) => {
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 h-full">
|
||||
<div class="flex justify-between text-xs">
|
||||
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
|
||||
<div>{{ t('apiNodesCostBreakdown.costPerRun') }}</div>
|
||||
</div>
|
||||
<ScrollPanel class="flex-grow h-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="node in nodes"
|
||||
:key="node.name"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-medium leading-tight">{{
|
||||
node.name
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-base font-medium leading-tight">
|
||||
{{ node.cost.toFixed(costPrecision) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
<template v-if="showTotal && nodes.length > 1">
|
||||
<Divider class="my-2" />
|
||||
<div class="flex justify-between items-center border-t px-3">
|
||||
<span class="text-sm">{{ t('apiNodesCostBreakdown.totalCost') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-yellow-500 p-1"
|
||||
/>
|
||||
<span>{{ totalCost.toFixed(costPrecision) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ApiNodeCost } from '@/types/apiNodeTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
nodes,
|
||||
showTotal = true,
|
||||
costPrecision = 3
|
||||
} = defineProps<{
|
||||
nodes: ApiNodeCost[]
|
||||
showTotal?: boolean
|
||||
costPrecision?: number
|
||||
}>()
|
||||
|
||||
const totalCost = computed(() =>
|
||||
nodes.reduce((sum, node) => sum + node.cost, 0)
|
||||
)
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 h-full">
|
||||
<div class="flex text-xs">
|
||||
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
|
||||
</div>
|
||||
<ScrollPanel class="flex-grow h-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="nodeName in nodeNames"
|
||||
:key="nodeName"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-medium leading-tight">{{
|
||||
nodeName
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { nodeNames } = defineProps<{ nodeNames: string[] }>()
|
||||
</script>
|
||||
@@ -42,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="TFilter extends SearchFilter">
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import { useElementSize, useScroll, whenever } from '@vueuse/core'
|
||||
import { clamp, debounce } from 'lodash'
|
||||
import { clamp, debounce } from 'es-toolkit/compat'
|
||||
import { type CSSProperties, computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
type GridState = {
|
||||
|
||||
@@ -36,6 +36,7 @@ import ListBox from 'primevue/listbox'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ElectronFileDownload from '@/components/common/ElectronFileDownload.vue'
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -169,8 +169,8 @@ import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
import _ from 'lodash'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { cloneDeep } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { merge } from 'lodash'
|
||||
import { merge } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import {
|
||||
computed,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
// Mock debounce to execute immediately
|
||||
vi.mock('lodash', () => ({
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
}))
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<div class="w-[100%] flex justify-between items-center">
|
||||
<div class="flex justify-start items-center">
|
||||
<div class="w-1 h-6 rounded-md" />
|
||||
<div class="w-6 h-6 relative overflow-hidden">
|
||||
<i class="pi pi-box text-xl text-muted" style="opacity: 0.6" />
|
||||
</div>
|
||||
<div class="px-3 py-2 rounded-md flex justify-start items-start gap-2.5">
|
||||
<div class="text-right justify-start text-sm font-bold leading-none">
|
||||
{{ $t('manager.nodePack') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex justify-start items-center gap-3">
|
||||
<div
|
||||
v-if="nodePack.downloads"
|
||||
class="flex items-center text-sm text-muted tracking-tighter"
|
||||
>
|
||||
<i class="pi pi-download mr-2" />
|
||||
{{ $n(nodePack.downloads) }}
|
||||
</div>
|
||||
<template v-if="isInstalled">
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
</script>
|
||||
@@ -57,7 +57,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { stubTrue } from 'lodash'
|
||||
import { stubTrue } from 'es-toolkit/compat'
|
||||
import AutoComplete, {
|
||||
AutoCompleteOptionSelectEvent
|
||||
} from 'primevue/autocomplete'
|
||||
|
||||
@@ -139,6 +139,7 @@ import Message from 'primevue/message'
|
||||
import Tag from 'primevue/tag'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { useKeybindingService } from '@/services/keybindingService'
|
||||
@@ -148,6 +149,7 @@ import {
|
||||
KeybindingImpl,
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
import PanelTemplate from './PanelTemplate.vue'
|
||||
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
|
||||
@@ -159,6 +161,7 @@ const filters = ref({
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const keybindingService = useKeybindingService()
|
||||
const commandStore = useCommandStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
interface ICommandData {
|
||||
id: string
|
||||
@@ -170,7 +173,10 @@ interface ICommandData {
|
||||
const commandsData = computed<ICommandData[]>(() => {
|
||||
return Object.values(commandStore.commands).map((command) => ({
|
||||
id: command.id,
|
||||
label: command.getTranslatedLabel(),
|
||||
label: t(
|
||||
`commands.${normalizeI18nKey(command.id)}.label`,
|
||||
command.label ?? ''
|
||||
),
|
||||
keybinding: keybindingStore.getKeybindingByCommandId(command.id),
|
||||
source: command.source
|
||||
}))
|
||||
|
||||
@@ -1,39 +1,68 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible && initialized"
|
||||
ref="containerRef"
|
||||
class="litegraph-minimap absolute right-[90px] z-[1000]"
|
||||
:class="{
|
||||
'bottom-[20px]': !bottomPanelStore.bottomPanelVisible,
|
||||
'bottom-[280px]': bottomPanelStore.bottomPanelVisible
|
||||
}"
|
||||
:style="containerStyles"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@pointerleave="handlePointerUp"
|
||||
@wheel="handleWheel"
|
||||
class="minimap-main-container flex absolute bottom-[20px] right-[90px] z-[1000]"
|
||||
>
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="minimap-canvas"
|
||||
<MiniMapPanel
|
||||
v-if="showOptionsPanel"
|
||||
:panel-styles="panelStyles"
|
||||
:node-colors="nodeColors"
|
||||
:show-links="showLinks"
|
||||
:show-groups="showGroups"
|
||||
:render-bypass="renderBypass"
|
||||
:render-error="renderError"
|
||||
@update-option="updateOption"
|
||||
/>
|
||||
<div class="minimap-viewport" :style="viewportStyles" />
|
||||
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="litegraph-minimap relative"
|
||||
:style="containerStyles"
|
||||
>
|
||||
<Button
|
||||
class="absolute z-10"
|
||||
size="small"
|
||||
text
|
||||
severity="secondary"
|
||||
@click.stop="toggleOptionsPanel"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:settings-2 />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="minimap-canvas"
|
||||
/>
|
||||
|
||||
<div class="minimap-viewport" :style="viewportStyles" />
|
||||
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@pointerleave="handlePointerUp"
|
||||
@wheel="handleWheel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { useMinimap } from '@/composables/useMinimap'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
|
||||
import MiniMapPanel from './MiniMapPanel.vue'
|
||||
|
||||
const minimap = useMinimap()
|
||||
const canvasStore = useCanvasStore()
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
|
||||
const {
|
||||
initialized,
|
||||
@@ -44,6 +73,13 @@ const {
|
||||
viewportStyles,
|
||||
width,
|
||||
height,
|
||||
panelStyles,
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
updateOption,
|
||||
init,
|
||||
destroy,
|
||||
handlePointerDown,
|
||||
@@ -52,6 +88,12 @@ const {
|
||||
handleWheel
|
||||
} = minimap
|
||||
|
||||
const showOptionsPanel = ref(false)
|
||||
|
||||
const toggleOptionsPanel = () => {
|
||||
showOptionsPanel.value = !showOptionsPanel.value
|
||||
}
|
||||
|
||||
watch(
|
||||
() => canvasStore.canvas,
|
||||
async (canvas) => {
|
||||
|
||||
97
src/components/graph/MiniMapPanel.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div
|
||||
class="minimap-panel p-3 mr-2 flex flex-col gap-3 text-sm"
|
||||
:style="panelStyles"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="node-colors"
|
||||
name="node-colors"
|
||||
:model-value="nodeColors"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) => $emit('updateOption', 'Comfy.Minimap.NodeColors', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:palette />
|
||||
<label for="node-colors">{{ $t('minimap.nodeColors') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="show-links"
|
||||
name="show-links"
|
||||
:model-value="showLinks"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) => $emit('updateOption', 'Comfy.Minimap.ShowLinks', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:route />
|
||||
<label for="show-links">{{ $t('minimap.showLinks') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="show-groups"
|
||||
name="show-groups"
|
||||
:model-value="showGroups"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) => $emit('updateOption', 'Comfy.Minimap.ShowGroups', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:frame />
|
||||
<label for="show-groups">{{ $t('minimap.showGroups') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="render-bypass"
|
||||
name="render-bypass"
|
||||
:model-value="renderBypass"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) =>
|
||||
$emit('updateOption', 'Comfy.Minimap.RenderBypassState', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:circle-slash-2 />
|
||||
<label for="render-bypass">{{ $t('minimap.renderBypassState') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="render-error"
|
||||
name="render-error"
|
||||
:model-value="renderError"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) =>
|
||||
$emit('updateOption', 'Comfy.Minimap.RenderErrorState', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:message-circle-warning />
|
||||
<label for="render-error">{{ $t('minimap.renderErrorState') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
|
||||
import { MinimapOptionKey } from '@/composables/useMinimap'
|
||||
|
||||
defineProps<{
|
||||
panelStyles: any
|
||||
nodeColors: boolean
|
||||
showLinks: boolean
|
||||
showGroups: boolean
|
||||
renderBypass: boolean
|
||||
renderError: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
updateOption: [key: MinimapOptionKey, value: boolean]
|
||||
}>()
|
||||
</script>
|
||||
@@ -13,6 +13,7 @@
|
||||
<BypassButton />
|
||||
<PinButton />
|
||||
<EditModelButton />
|
||||
<Load3DViewerButton />
|
||||
<MaskEditorButton />
|
||||
<ConvertToSubgraphButton />
|
||||
<DeleteButton />
|
||||
@@ -38,6 +39,7 @@ import EditModelButton from '@/components/graph/selectionToolbox/EditModelButton
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
|
||||
import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'
|
||||
import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewerButton.vue'
|
||||
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
||||
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
||||
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="isVisible"
|
||||
v-if="isUnpackVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_UnpackSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.Graph.UnpackSubgraph')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:expand />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="isConvertVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_ConvertToSubgraph.label'),
|
||||
showDelay: 1000
|
||||
@@ -20,6 +34,7 @@ import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
@@ -27,7 +42,13 @@ const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const isVisible = computed(() => {
|
||||
const isUnpackVisible = computed(() => {
|
||||
return (
|
||||
canvasStore.selectedItems?.length === 1 &&
|
||||
canvasStore.selectedItems[0] instanceof SubgraphNode
|
||||
)
|
||||
})
|
||||
const isConvertVisible = computed(() => {
|
||||
return (
|
||||
canvasStore.groupSelected ||
|
||||
canvasStore.rerouteSelected ||
|
||||
|
||||
38
src/components/graph/selectionToolbox/Load3DViewerButton.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="is3DNode"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_3DViewer_Open3DViewer.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
icon="pi pi-pencil"
|
||||
@click="open3DViewer"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { isLGraphNode, isLoad3dNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const is3DNode = computed(() => {
|
||||
const enable3DViewer = useSettingStore().get('Comfy.Load3D.3DViewerEnable')
|
||||
const nodes = canvasStore.selectedItems.filter(isLGraphNode)
|
||||
|
||||
return nodes.length === 1 && nodes.some(isLoad3dNode) && enable3DViewer
|
||||
})
|
||||
|
||||
const open3DViewer = () => {
|
||||
void commandStore.execute('Comfy.3DViewer.Open3DViewer')
|
||||
}
|
||||
</script>
|
||||
@@ -58,8 +58,19 @@
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
<div
|
||||
v-if="showRecordingControls"
|
||||
v-if="enable3DViewer"
|
||||
class="absolute top-12 right-2 z-20 pointer-events-auto"
|
||||
>
|
||||
<ViewerControls :node="node" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showRecordingControls"
|
||||
class="absolute right-2 z-20 pointer-events-auto"
|
||||
:class="{
|
||||
'top-12': !enable3DViewer,
|
||||
'top-24': enable3DViewer
|
||||
}"
|
||||
>
|
||||
<RecordingControls
|
||||
:node="node"
|
||||
@@ -82,6 +93,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import Load3DControls from '@/components/load3d/Load3DControls.vue'
|
||||
import Load3DScene from '@/components/load3d/Load3DScene.vue'
|
||||
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
|
||||
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import {
|
||||
CameraType,
|
||||
@@ -91,6 +103,7 @@ import {
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -121,6 +134,9 @@ const isRecording = ref(false)
|
||||
const hasRecording = ref(false)
|
||||
const recordingDuration = ref(0)
|
||||
const showRecordingControls = ref(!inputSpec.isPreview)
|
||||
const enable3DViewer = computed(() =>
|
||||
useSettingStore().get('Comfy.Load3D.3DViewerEnable')
|
||||
)
|
||||
|
||||
const showPreviewButton = computed(() => {
|
||||
return !type.includes('Preview')
|
||||
|
||||
149
src/components/load3d/Load3dViewerContent.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div
|
||||
ref="viewerContentRef"
|
||||
class="flex w-full"
|
||||
:class="[maximized ? 'h-full' : 'h-[70vh]']"
|
||||
@mouseenter="viewer.handleMouseEnter"
|
||||
@mouseleave="viewer.handleMouseLeave"
|
||||
>
|
||||
<div ref="mainContentRef" class="flex-1 relative">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="absolute w-full h-full comfy-load-3d-viewer"
|
||||
@resize="viewer.handleResize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-72 flex flex-col">
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<div class="space-y-2">
|
||||
<div class="p-2 space-y-4">
|
||||
<SceneControls
|
||||
v-model:background-color="viewer.backgroundColor.value"
|
||||
v-model:show-grid="viewer.showGrid.value"
|
||||
:has-background-image="viewer.hasBackgroundImage.value"
|
||||
@update-background-image="viewer.handleBackgroundImageUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2 space-y-4">
|
||||
<ModelControls
|
||||
v-model:up-direction="viewer.upDirection.value"
|
||||
v-model:material-mode="viewer.materialMode.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2 space-y-4">
|
||||
<CameraControls
|
||||
v-model:camera-type="viewer.cameraType.value"
|
||||
v-model:fov="viewer.fov.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2 space-y-4">
|
||||
<LightControls
|
||||
v-model:light-intensity="viewer.lightIntensity.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2 space-y-4">
|
||||
<ExportControls @export-model="viewer.exportModel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
:label="t('g.cancel')"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { onBeforeUnmount, onMounted, ref, toRaw } from 'vue'
|
||||
|
||||
import CameraControls from '@/components/load3d/controls/viewer/CameraControls.vue'
|
||||
import ExportControls from '@/components/load3d/controls/viewer/ExportControls.vue'
|
||||
import LightControls from '@/components/load3d/controls/viewer/LightControls.vue'
|
||||
import ModelControls from '@/components/load3d/controls/viewer/ModelControls.vue'
|
||||
import SceneControls from '@/components/load3d/controls/viewer/SceneControls.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const props = defineProps<{
|
||||
node: LGraphNode
|
||||
}>()
|
||||
|
||||
const viewerContentRef = ref<HTMLDivElement>()
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const mainContentRef = ref<HTMLDivElement>()
|
||||
const maximized = ref(false)
|
||||
const mutationObserver = ref<MutationObserver | null>(null)
|
||||
|
||||
const viewer = useLoad3dService().getOrCreateViewer(toRaw(props.node))
|
||||
|
||||
onMounted(async () => {
|
||||
const source = useLoad3dService().getLoad3d(props.node)
|
||||
if (source && containerRef.value) {
|
||||
await viewer.initializeViewer(containerRef.value, source)
|
||||
}
|
||||
|
||||
if (viewerContentRef.value) {
|
||||
mutationObserver.value = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
mutation.type === 'attributes' &&
|
||||
mutation.attributeName === 'maximized'
|
||||
) {
|
||||
maximized.value =
|
||||
(mutation.target as HTMLElement).getAttribute('maximized') ===
|
||||
'true'
|
||||
|
||||
setTimeout(() => {
|
||||
viewer.refreshViewport()
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
mutationObserver.value.observe(viewerContentRef.value, {
|
||||
attributes: true,
|
||||
attributeFilter: ['maximized']
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('resize', viewer.handleResize)
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
viewer.restoreInitialState()
|
||||
useDialogStore().closeDialog()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', viewer.handleResize)
|
||||
|
||||
if (mutationObserver.value) {
|
||||
mutationObserver.value.disconnect()
|
||||
mutationObserver.value = null
|
||||
}
|
||||
|
||||
// we will manually cleanup the viewer in dialog close handler
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-panel-content) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
52
src/components/load3d/controls/ViewerControls.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="relative bg-gray-700 bg-opacity-30 rounded-lg">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button class="p-button-rounded p-button-text" @click="openIn3DViewer">
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: t('load3d.openIn3DViewer'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-expand text-white text-lg"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const { node } = defineProps<{
|
||||
node: LGraphNode
|
||||
}>()
|
||||
|
||||
const openIn3DViewer = () => {
|
||||
const props = { node: node }
|
||||
|
||||
useDialogStore().showDialog({
|
||||
key: 'global-load3d-viewer',
|
||||
title: t('load3d.viewer.title'),
|
||||
component: Load3DViewerContent,
|
||||
props: props,
|
||||
dialogComponentProps: {
|
||||
style: 'width: 80vw; height: 80vh;',
|
||||
maximizable: true,
|
||||
onClose: async () => {
|
||||
await useLoad3dService().handleViewerClose(props.node)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
37
src/components/load3d/controls/viewer/CameraControls.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<label>
|
||||
{{ t('load3d.viewer.cameraType') }}
|
||||
</label>
|
||||
<Select
|
||||
v-model="cameraType"
|
||||
:options="cameras"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div v-if="showFOVButton" class="space-y-4">
|
||||
<label>{{ t('load3d.fov') }}</label>
|
||||
<Slider v-model="fov" :min="10" :max="150" :step="1" aria-label="fov" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const cameras = [
|
||||
{ title: t('load3d.cameraType.perspective'), value: 'perspective' },
|
||||
{ title: t('load3d.cameraType.orthographic'), value: 'orthographic' }
|
||||
]
|
||||
|
||||
const cameraType = defineModel<CameraType>('cameraType')
|
||||
const fov = defineModel<number>('fov')
|
||||
const showFOVButton = computed(() => cameraType.value === 'perspective')
|
||||
</script>
|
||||
37
src/components/load3d/controls/viewer/ExportControls.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<Select
|
||||
v-model="exportFormat"
|
||||
:options="exportFormats"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
>
|
||||
</Select>
|
||||
|
||||
<Button severity="secondary" text rounded @click="exportModel(exportFormat)">
|
||||
{{ t('load3d.export') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Select from 'primevue/select'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'exportModel', format: string): void
|
||||
}>()
|
||||
|
||||
const exportFormats = [
|
||||
{ label: 'GLB', value: 'glb' },
|
||||
{ label: 'OBJ', value: 'obj' },
|
||||
{ label: 'STL', value: 'stl' }
|
||||
]
|
||||
|
||||
const exportFormat = ref('obj')
|
||||
|
||||
const exportModel = (format: string) => {
|
||||
emit('exportModel', format)
|
||||
}
|
||||
</script>
|
||||
30
src/components/load3d/controls/viewer/LightControls.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<label>{{ t('load3d.lightIntensity') }}</label>
|
||||
|
||||
<Slider
|
||||
v-model="lightIntensity"
|
||||
class="w-full"
|
||||
:min="lightIntensityMinimum"
|
||||
:max="lightIntensityMaximum"
|
||||
:step="lightAdjustmentIncrement"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Slider from 'primevue/slider'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const lightIntensity = defineModel<number>('lightIntensity')
|
||||
|
||||
const lightIntensityMaximum = useSettingStore().get(
|
||||
'Comfy.Load3D.LightIntensityMaximum'
|
||||
)
|
||||
const lightIntensityMinimum = useSettingStore().get(
|
||||
'Comfy.Load3D.LightIntensityMinimum'
|
||||
)
|
||||
const lightAdjustmentIncrement = useSettingStore().get(
|
||||
'Comfy.Load3D.LightAdjustmentIncrement'
|
||||
)
|
||||
</script>
|
||||
52
src/components/load3d/controls/viewer/ModelControls.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label>{{ t('load3d.upDirection') }}</label>
|
||||
<Select
|
||||
v-model="upDirection"
|
||||
:options="upDirectionOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>{{ t('load3d.materialMode') }}</label>
|
||||
<Select
|
||||
v-model="materialMode"
|
||||
:options="materialModeOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { MaterialMode, UpDirection } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const upDirection = defineModel<UpDirection>('upDirection')
|
||||
const materialMode = defineModel<MaterialMode>('materialMode')
|
||||
|
||||
const upDirectionOptions = [
|
||||
{ label: t('load3d.upDirections.original'), value: 'original' },
|
||||
{ label: '-X', value: '-x' },
|
||||
{ label: '+X', value: '+x' },
|
||||
{ label: '-Y', value: '-y' },
|
||||
{ label: '+Y', value: '+y' },
|
||||
{ label: '-Z', value: '-z' },
|
||||
{ label: '+Z', value: '+z' }
|
||||
]
|
||||
|
||||
const materialModeOptions = computed(() => {
|
||||
return [
|
||||
{ label: t('load3d.materialModes.original'), value: 'original' },
|
||||
{ label: t('load3d.materialModes.normal'), value: 'normal' },
|
||||
{ label: t('load3d.materialModes.wireframe'), value: 'wireframe' }
|
||||
]
|
||||
})
|
||||
</script>
|
||||
82
src/components/load3d/controls/viewer/SceneControls.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div v-if="!hasBackgroundImage">
|
||||
<label>
|
||||
{{ t('load3d.backgroundColor') }}
|
||||
</label>
|
||||
<input v-model="backgroundColor" type="color" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Checkbox v-model="showGrid" input-id="showGrid" binary name="showGrid" />
|
||||
<label for="showGrid" class="pl-2">
|
||||
{{ t('load3d.showGrid') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasBackgroundImage">
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="t('load3d.uploadBackgroundImage')"
|
||||
icon="pi pi-image"
|
||||
class="w-full"
|
||||
@click="openImagePicker"
|
||||
/>
|
||||
<input
|
||||
ref="imagePickerRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="hasBackgroundImage" class="space-y-2">
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="t('load3d.removeBackgroundImage')"
|
||||
icon="pi pi-times"
|
||||
class="w-full"
|
||||
@click="removeBackgroundImage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const backgroundColor = defineModel<string>('backgroundColor')
|
||||
const showGrid = defineModel<boolean>('showGrid')
|
||||
|
||||
defineProps<{
|
||||
hasBackgroundImage?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateBackgroundImage', file: File | null): void
|
||||
}>()
|
||||
|
||||
const imagePickerRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const openImagePicker = () => {
|
||||
imagePickerRef.value?.click()
|
||||
}
|
||||
|
||||
const handleImageUpload = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (input.files && input.files[0]) {
|
||||
emit('updateBackgroundImage', input.files[0])
|
||||
}
|
||||
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
const removeBackgroundImage = () => {
|
||||
emit('updateBackgroundImage', null)
|
||||
}
|
||||
</script>
|
||||
@@ -82,7 +82,7 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-3 px-3 py-2 w-full">
|
||||
<span
|
||||
class="flex-shrink-0 w-5 text-center text-muted item-icon"
|
||||
:class="command.icon ?? 'pi pi-chevron-right'"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="flex-grow overflow-hidden text-ellipsis whitespace-nowrap item-label"
|
||||
>
|
||||
<span
|
||||
v-html="highlightQuery(command.getTranslatedLabel(), currentQuery)"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="command.keybinding"
|
||||
class="flex-shrink-0 text-xs px-1.5 py-0.5 border rounded font-mono keybinding-badge"
|
||||
>
|
||||
{{ command.keybinding.combo.toString() }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
import { highlightQuery } from '@/utils/formatUtil'
|
||||
|
||||
const { command, currentQuery } = defineProps<{
|
||||
command: ComfyCommandImpl
|
||||
currentQuery: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.highlight) {
|
||||
background-color: var(--p-primary-color);
|
||||
color: var(--p-primary-contrast-color);
|
||||
font-weight: bold;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0 0.125rem;
|
||||
margin: -0.125rem 0.125rem;
|
||||
}
|
||||
|
||||
.keybinding-badge {
|
||||
border-color: var(--p-content-border-color);
|
||||
background-color: var(--p-content-hover-background);
|
||||
color: var(--p-text-muted-color);
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
class="comfy-vue-node-search-container flex justify-center items-center w-full min-w-96"
|
||||
>
|
||||
<div
|
||||
v-if="enableNodePreview && !isCommandMode"
|
||||
v-if="enableNodePreview"
|
||||
class="comfy-vue-node-preview-container absolute left-[-350px] top-[50px]"
|
||||
>
|
||||
<NodePreview
|
||||
@@ -14,7 +14,6 @@
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="!isCommandMode"
|
||||
icon="pi pi-filter"
|
||||
severity="secondary"
|
||||
class="filter-button z-10"
|
||||
@@ -50,24 +49,13 @@
|
||||
auto-option-focus
|
||||
force-selection
|
||||
multiple
|
||||
:option-label="getOptionLabel"
|
||||
:option-label="'display_name'"
|
||||
@complete="search($event.query)"
|
||||
@option-select="onOptionSelect($event.value)"
|
||||
@option-select="emit('addNode', $event.value)"
|
||||
@focused-option-changed="setHoverSuggestion($event)"
|
||||
@input="handleInput"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<!-- Command search item, Remove the '>' prefix from the query -->
|
||||
<CommandSearchItem
|
||||
v-if="isCommandMode"
|
||||
:command="option"
|
||||
:current-query="currentQuery.substring(1)"
|
||||
/>
|
||||
<NodeSearchItem
|
||||
v-else
|
||||
:node-def="option"
|
||||
:current-query="currentQuery"
|
||||
/>
|
||||
<NodeSearchItem :node-def="option" :current-query="currentQuery" />
|
||||
</template>
|
||||
<!-- FilterAndValue -->
|
||||
<template #chip="{ value }">
|
||||
@@ -92,16 +80,13 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
|
||||
import CommandSearchItem from '@/components/searchbox/CommandSearchItem.vue'
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
|
||||
import { CommandSearchService } from '@/services/commandSearchService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore,
|
||||
@@ -114,7 +99,6 @@ import SearchFilterChip from '../common/SearchFilterChip.vue'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const enableNodePreview = computed(() =>
|
||||
settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview')
|
||||
@@ -127,50 +111,18 @@ const { filters, searchLimit = 64 } = defineProps<{
|
||||
|
||||
const nodeSearchFilterVisible = ref(false)
|
||||
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`
|
||||
const suggestions = ref<ComfyNodeDefImpl[] | ComfyCommandImpl[]>([])
|
||||
const suggestions = ref<ComfyNodeDefImpl[]>([])
|
||||
const hoveredSuggestion = ref<ComfyNodeDefImpl | null>(null)
|
||||
const currentQuery = ref('')
|
||||
const isCommandMode = ref(false)
|
||||
|
||||
// Initialize command search service
|
||||
const commandSearchService = ref<CommandSearchService | null>(null)
|
||||
|
||||
const placeholder = computed(() => {
|
||||
if (isCommandMode.value) {
|
||||
return t('g.searchCommands', 'Search commands') + '...'
|
||||
}
|
||||
return filters.length === 0 ? t('g.searchNodes') + '...' : ''
|
||||
})
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeFrequencyStore = useNodeFrequencyStore()
|
||||
|
||||
// Initialize command search service with commands
|
||||
watch(
|
||||
() => commandStore.commands,
|
||||
(commands) => {
|
||||
commandSearchService.value = new CommandSearchService(commands)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const search = (query: string) => {
|
||||
currentQuery.value = query
|
||||
|
||||
// Check if we're in command mode (query starts with ">")
|
||||
if (query.startsWith('>')) {
|
||||
isCommandMode.value = true
|
||||
if (commandSearchService.value) {
|
||||
suggestions.value = commandSearchService.value.searchCommands(query, {
|
||||
limit: searchLimit
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Normal node search mode
|
||||
isCommandMode.value = false
|
||||
const queryIsEmpty = query === '' && filters.length === 0
|
||||
currentQuery.value = query
|
||||
suggestions.value = queryIsEmpty
|
||||
? nodeFrequencyStore.topNodeDefs
|
||||
: [
|
||||
@@ -180,18 +132,7 @@ const search = (query: string) => {
|
||||
]
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'addFilter',
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
): void
|
||||
(
|
||||
e: 'removeFilter',
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
): void
|
||||
(e: 'addNode', nodeDef: ComfyNodeDefImpl): void
|
||||
(e: 'executeCommand', command: ComfyCommandImpl): void
|
||||
}>()
|
||||
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
|
||||
|
||||
let inputElement: HTMLInputElement | null = null
|
||||
const reFocusInput = async () => {
|
||||
@@ -219,47 +160,11 @@ const onRemoveFilter = async (
|
||||
await reFocusInput()
|
||||
}
|
||||
const setHoverSuggestion = (index: number) => {
|
||||
if (index === -1 || isCommandMode.value) {
|
||||
if (index === -1) {
|
||||
hoveredSuggestion.value = null
|
||||
return
|
||||
}
|
||||
const value = suggestions.value[index] as ComfyNodeDefImpl
|
||||
const value = suggestions.value[index]
|
||||
hoveredSuggestion.value = value
|
||||
}
|
||||
|
||||
const onOptionSelect = (option: ComfyNodeDefImpl | ComfyCommandImpl) => {
|
||||
if (isCommandMode.value) {
|
||||
emit('executeCommand', option as ComfyCommandImpl)
|
||||
} else {
|
||||
emit('addNode', option as ComfyNodeDefImpl)
|
||||
}
|
||||
}
|
||||
|
||||
const getOptionLabel = (
|
||||
option: ComfyNodeDefImpl | ComfyCommandImpl
|
||||
): string => {
|
||||
if ('display_name' in option) {
|
||||
return option.display_name
|
||||
}
|
||||
return option.label || option.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles direct input changes on the AutoCompletePlus component.
|
||||
* This ensures search mode switching works properly when users clear the input
|
||||
* or modify it directly, as the @complete event may not always trigger.
|
||||
*
|
||||
* @param event - The input event from the AutoCompletePlus component
|
||||
* @note Known issue on empty input complete state:
|
||||
* https://github.com/Comfy-Org/ComfyUI_frontend/issues/4887
|
||||
*/
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const inputValue = target.value
|
||||
|
||||
// Trigger search to handle mode switching between node and command search
|
||||
if (inputValue === '') {
|
||||
search('')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
@add-filter="addFilter"
|
||||
@remove-filter="removeFilter"
|
||||
@add-node="addNode"
|
||||
@execute-command="executeCommand"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -47,7 +46,6 @@ import {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
@@ -64,7 +62,6 @@ let disconnectOnReset = false
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const { visible } = storeToRefs(useSearchBoxStore())
|
||||
const dismissable = ref(true)
|
||||
@@ -93,18 +90,16 @@ const closeDialog = () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const addNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
if (!triggerEvent) {
|
||||
console.warn('The trigger event was undefined when addNode was called.')
|
||||
return
|
||||
}
|
||||
|
||||
const node = litegraphService.addNodeOnGraph(nodeDef, {
|
||||
pos: getNewNodeLocation()
|
||||
})
|
||||
|
||||
if (disconnectOnReset) {
|
||||
if (disconnectOnReset && triggerEvent) {
|
||||
canvasStore.getCanvas().linkConnector.connectToNode(node, triggerEvent)
|
||||
} else if (!triggerEvent) {
|
||||
console.warn('The trigger event was undefined when addNode was called.')
|
||||
}
|
||||
|
||||
disconnectOnReset = false
|
||||
|
||||
// Notify changeTracker - new step should be added
|
||||
@@ -112,14 +107,6 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
window.requestAnimationFrame(closeDialog)
|
||||
}
|
||||
|
||||
const executeCommand = async (command: ComfyCommandImpl) => {
|
||||
// Close the dialog immediately
|
||||
closeDialog()
|
||||
|
||||
// Execute the command
|
||||
await commandStore.execute(command.id)
|
||||
}
|
||||
|
||||
const newSearchBoxEnabled = computed(
|
||||
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
|
||||
)
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
:icon-badge="tab.iconBadge"
|
||||
:tooltip="tab.tooltip"
|
||||
:tooltip-suffix="getTabTooltipSuffix(tab)"
|
||||
:label="tab.label || tab.title"
|
||||
:is-small="isSmall"
|
||||
:selected="tab.id === selectedTab?.id"
|
||||
:class="tab.id + '-tab-button'"
|
||||
@click="onTabClick(tab)"
|
||||
/>
|
||||
<SidebarTemplatesButton />
|
||||
<div class="side-tool-bar-end">
|
||||
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
|
||||
<SidebarHelpCenterIcon />
|
||||
@@ -43,6 +46,7 @@ import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
import SidebarLogoutIcon from './SidebarLogoutIcon.vue'
|
||||
import SidebarTemplatesButton from './SidebarTemplatesButton.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -86,7 +90,7 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
box-shadow: var(--bar-shadow);
|
||||
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-icon-size: 1.5rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
}
|
||||
|
||||
.side-tool-bar-container.small-sidebar {
|
||||
|
||||
@@ -58,11 +58,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
||||
import ReleaseNotificationToast from '@/components/helpcenter/ReleaseNotificationToast.vue'
|
||||
import WhatsNewPopup from '@/components/helpcenter/WhatsNewPopup.vue'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -70,8 +71,9 @@ import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const releaseStore = useReleaseStore()
|
||||
const helpCenterStore = useHelpCenterStore()
|
||||
const { shouldShowRedDot } = storeToRefs(releaseStore)
|
||||
const isHelpCenterVisible = ref(false)
|
||||
const { isVisible: isHelpCenterVisible } = storeToRefs(helpCenterStore)
|
||||
|
||||
const sidebarLocation = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
@@ -80,11 +82,11 @@ const sidebarLocation = computed(() =>
|
||||
const sidebarSize = computed(() => settingStore.get('Comfy.Sidebar.Size'))
|
||||
|
||||
const toggleHelpCenter = () => {
|
||||
isHelpCenterVisible.value = !isHelpCenterVisible.value
|
||||
helpCenterStore.toggle()
|
||||
}
|
||||
|
||||
const closeHelpCenter = () => {
|
||||
isHelpCenterVisible.value = false
|
||||
helpCenterStore.hide()
|
||||
}
|
||||
|
||||
// Initialize release store on mount
|
||||
@@ -130,6 +132,7 @@ onMounted(async () => {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
@@ -19,12 +19,29 @@
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<template #icon>
|
||||
<slot name="icon">
|
||||
<OverlayBadge v-if="shouldShowBadge" :value="overlayValue">
|
||||
<i :class="icon + ' side-bar-button-icon'" />
|
||||
</OverlayBadge>
|
||||
<i v-else :class="icon + ' side-bar-button-icon'" />
|
||||
</slot>
|
||||
<div class="side-bar-button-content">
|
||||
<slot name="icon">
|
||||
<OverlayBadge v-if="shouldShowBadge" :value="overlayValue">
|
||||
<i
|
||||
v-if="typeof icon === 'string'"
|
||||
:class="icon + ' side-bar-button-icon'"
|
||||
/>
|
||||
<component :is="icon" v-else class="side-bar-button-icon" />
|
||||
</OverlayBadge>
|
||||
<i
|
||||
v-else-if="typeof icon === 'string'"
|
||||
:class="icon + ' side-bar-button-icon'"
|
||||
/>
|
||||
<component
|
||||
:is="icon"
|
||||
v-else-if="typeof icon === 'object'"
|
||||
class="side-bar-button-icon"
|
||||
/>
|
||||
</slot>
|
||||
<span v-if="label && !isSmall" class="side-bar-button-label">{{
|
||||
t(label)
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -33,6 +50,7 @@
|
||||
import Button from 'primevue/button'
|
||||
import OverlayBadge from 'primevue/overlaybadge'
|
||||
import { computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -41,13 +59,17 @@ const {
|
||||
selected = false,
|
||||
tooltip = '',
|
||||
tooltipSuffix = '',
|
||||
iconBadge = ''
|
||||
iconBadge = '',
|
||||
label = '',
|
||||
isSmall = false
|
||||
} = defineProps<{
|
||||
icon?: string
|
||||
icon?: string | Component
|
||||
selected?: boolean
|
||||
tooltip?: string
|
||||
tooltipSuffix?: string
|
||||
iconBadge?: string | (() => string | null)
|
||||
label?: string
|
||||
isSmall?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -74,10 +96,23 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
|
||||
<style scoped>
|
||||
.side-bar-button {
|
||||
width: var(--sidebar-width);
|
||||
height: var(--sidebar-width);
|
||||
height: calc(var(--sidebar-width) + 0.5rem);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.side-tool-bar-end .side-bar-button {
|
||||
height: var(--sidebar-width);
|
||||
}
|
||||
|
||||
.side-bar-button-content {
|
||||
@apply flex flex-col items-center gap-2;
|
||||
}
|
||||
|
||||
.side-bar-button-label {
|
||||
@apply text-[10px] text-center whitespace-nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.comfyui-body-left .side-bar-button.side-bar-button-selected,
|
||||
.comfyui-body-left .side-bar-button.side-bar-button-selected:hover {
|
||||
border-left: 4px solid var(--p-button-text-primary-color);
|
||||
|
||||
35
src/components/sidebar/SidebarTemplatesButton.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
:icon="TemplateIcon"
|
||||
:tooltip="$t('sideToolbar.templates')"
|
||||
:label="$t('sideToolbar.labels.templates')"
|
||||
:is-small="isSmall"
|
||||
class="templates-tab-button"
|
||||
@click="openTemplates"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
// Import the custom template icon
|
||||
const TemplateIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/template'))
|
||||
)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const isSmall = computed(
|
||||
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
|
||||
)
|
||||
|
||||
const openTemplates = () => {
|
||||
void commandStore.execute('Comfy.BrowseTemplates')
|
||||
}
|
||||
</script>
|
||||
@@ -30,11 +30,18 @@
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.resetView')"
|
||||
icon="pi pi-refresh"
|
||||
icon="pi pi-filter-slash"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="resetOrganization"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.refresh')"
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="() => commandStore.execute('Comfy.RefreshNodeDefinitions')"
|
||||
/>
|
||||
<Popover ref="groupingPopover">
|
||||
<div class="flex flex-col gap-1 p-2">
|
||||
<Button
|
||||
@@ -139,6 +146,7 @@ import {
|
||||
DEFAULT_SORTING_ID,
|
||||
nodeOrganizationService
|
||||
} from '@/services/nodeOrganizationService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
|
||||
@@ -155,6 +163,7 @@ import NodeBookmarkTreeExplorer from './nodeLibrary/NodeBookmarkTreeExplorer.vue
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
const nodeHelpStore = useNodeHelpStore()
|
||||
const commandStore = useCommandStore()
|
||||
const expandedKeys = ref<Record<string, boolean>>({})
|
||||
const { expandNode, toggleNodeOnEvent } = useTreeExpansion(expandedKeys)
|
||||
|
||||
|
||||
@@ -265,6 +265,14 @@ const renderTreeNode = (
|
||||
const workflow = node.data
|
||||
await workflowService.insertWorkflow(workflow)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('g.duplicate'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: async () => {
|
||||
const workflow = node.data
|
||||
await workflowService.duplicateWorkflow(workflow)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -55,9 +55,30 @@
|
||||
v-bind="props.action"
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
:class="typeof item.class === 'function' ? item.class() : item.class"
|
||||
@mousedown="
|
||||
isZoomCommand(item) ? handleZoomMouseDown(item, $event) : undefined
|
||||
"
|
||||
@click="isZoomCommand(item) ? handleZoomClick($event) : undefined"
|
||||
>
|
||||
<span v-if="item.icon" class="p-menubar-item-icon" :class="item.icon" />
|
||||
<i
|
||||
v-if="hasActiveStateSiblings(item)"
|
||||
class="p-menubar-item-icon pi pi-check text-sm"
|
||||
:class="{ invisible: !item.comfyCommand?.active?.() }"
|
||||
/>
|
||||
<span
|
||||
v-else-if="
|
||||
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
|
||||
"
|
||||
class="p-menubar-item-icon"
|
||||
:class="item.icon"
|
||||
/>
|
||||
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
|
||||
<i
|
||||
v-if="item.comfyCommand?.id === 'Comfy.NewBlankWorkflow'"
|
||||
class="ml-auto"
|
||||
:class="item.icon"
|
||||
/>
|
||||
<span
|
||||
v-if="item?.comfyCommand?.keybinding"
|
||||
class="ml-auto border border-surface rounded text-muted text-xs text-nowrap p-1 keybinding-tag"
|
||||
@@ -94,6 +115,7 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { showNativeSystemMenu } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const menuItemsStore = useMenuItemStore()
|
||||
@@ -163,16 +185,22 @@ const extraMenuItems: MenuItem[] = [
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
key: 'manage-extensions',
|
||||
label: t('menu.manageExtensions'),
|
||||
icon: 'mdi mdi-puzzle-outline',
|
||||
command: showManageExtensions
|
||||
key: 'browse-templates',
|
||||
label: t('menuLabels.Browse Templates'),
|
||||
icon: 'pi pi-folder-open',
|
||||
command: () => commandStore.execute('Comfy.BrowseTemplates')
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: t('g.settings'),
|
||||
icon: 'mdi mdi-cog-outline',
|
||||
command: () => showSettings()
|
||||
},
|
||||
{
|
||||
key: 'manage-extensions',
|
||||
label: t('menu.manageExtensions'),
|
||||
icon: 'mdi mdi-puzzle-outline',
|
||||
command: showManageExtensions
|
||||
}
|
||||
]
|
||||
|
||||
@@ -237,6 +265,36 @@ const onMenuShow = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isZoomCommand = (item: MenuItem) => {
|
||||
return (
|
||||
item.comfyCommand?.id === 'Comfy.Canvas.ZoomIn' ||
|
||||
item.comfyCommand?.id === 'Comfy.Canvas.ZoomOut'
|
||||
)
|
||||
}
|
||||
|
||||
const handleZoomMouseDown = (item: MenuItem, event: MouseEvent) => {
|
||||
if (item.comfyCommand) {
|
||||
whileMouseDown(
|
||||
event,
|
||||
async () => {
|
||||
await commandStore.execute(item.comfyCommand!.id)
|
||||
},
|
||||
50
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleZoomClick = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// Prevent the menu from closing for zoom commands
|
||||
return false
|
||||
}
|
||||
|
||||
const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
return menuItemsStore.menuItemHasActiveStateChildren[item.parentPath]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { Ref, markRaw, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useTerminal(element: Ref<HTMLElement | undefined>) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutationObserver, useResizeObserver } from '@vueuse/core'
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { readonly, ref } from 'vue'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import { useNodePricing } from '@/composables/node/useNodePricing'
|
||||
|
||||
@@ -261,7 +261,10 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
return '$0.14-2.80/Run (varies with model, mode & duration)'
|
||||
|
||||
const modelValue = String(modelWidget.value)
|
||||
if (modelValue.includes('v2-master')) {
|
||||
if (
|
||||
modelValue.includes('v2-1-master') ||
|
||||
modelValue.includes('v2-master')
|
||||
) {
|
||||
return '$1.40/Run'
|
||||
} else if (
|
||||
modelValue.includes('v1-6') ||
|
||||
@@ -280,12 +283,19 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
console.log('durationValue', durationValue)
|
||||
|
||||
// Same pricing matrix as KlingTextToVideoNode
|
||||
if (modelValue.includes('v2-master')) {
|
||||
if (
|
||||
modelValue.includes('v2-1-master') ||
|
||||
modelValue.includes('v2-master')
|
||||
) {
|
||||
if (durationValue.includes('10')) {
|
||||
return '$2.80/Run'
|
||||
}
|
||||
return '$1.40/Run' // 5s default
|
||||
} else if (modelValue.includes('v1-6') || modelValue.includes('v1-5')) {
|
||||
} else if (
|
||||
modelValue.includes('v2-1') ||
|
||||
modelValue.includes('v1-6') ||
|
||||
modelValue.includes('v1-5')
|
||||
) {
|
||||
if (modeValue.includes('pro')) {
|
||||
return durationValue.includes('10') ? '$0.98/Run' : '$0.49/Run'
|
||||
} else {
|
||||
@@ -418,7 +428,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const modeValue = String(modeWidget.value)
|
||||
|
||||
// Pricing matrix from CSV data based on mode string content
|
||||
if (modeValue.includes('v2-master')) {
|
||||
if (modeValue.includes('v2-1-master')) {
|
||||
if (modeValue.includes('10s')) {
|
||||
return '$2.80/Run' // price is the same as for v2-master model
|
||||
}
|
||||
return '$1.40/Run' // price is the same as for v2-master model
|
||||
} else if (modeValue.includes('v2-master')) {
|
||||
if (modeValue.includes('10s')) {
|
||||
return '$2.80/Run'
|
||||
}
|
||||
@@ -558,6 +573,32 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
MinimaxTextToVideoNode: {
|
||||
displayPrice: '$0.43/Run'
|
||||
},
|
||||
MinimaxHailuoVideoNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const resolutionWidget = node.widgets?.find(
|
||||
(w) => w.name === 'resolution'
|
||||
) as IComboWidget
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
|
||||
if (!resolutionWidget || !durationWidget) {
|
||||
return '$0.28-0.56/Run (varies with resolution & duration)'
|
||||
}
|
||||
|
||||
const resolution = String(resolutionWidget.value)
|
||||
const duration = String(durationWidget.value)
|
||||
|
||||
if (resolution.includes('768P')) {
|
||||
if (duration.includes('6')) return '$0.28/Run'
|
||||
if (duration.includes('10')) return '$0.56/Run'
|
||||
} else if (resolution.includes('1080P')) {
|
||||
if (duration.includes('6')) return '$0.49/Run'
|
||||
}
|
||||
|
||||
return '$0.43/Run' // default median
|
||||
}
|
||||
},
|
||||
OpenAIDalle2: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const sizeWidget = node.widgets?.find(
|
||||
@@ -1278,9 +1319,13 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
// Google Veo video generation
|
||||
if (model.includes('veo-2.0')) {
|
||||
return '$0.5/second'
|
||||
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
|
||||
return '$0.00016/$0.0006 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-flash-preview-04-17')) {
|
||||
return '$0.0003/$0.0025 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-flash')) {
|
||||
return '$0.0003/$0.0025 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
|
||||
return '$0.00125/$0.01 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-pro')) {
|
||||
return '$0.00125/$0.01 per 1K tokens'
|
||||
}
|
||||
// For other Gemini models, show token-based pricing info
|
||||
@@ -1317,6 +1362,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
return '$0.0004/$0.0016 per 1K tokens'
|
||||
} else if (model.includes('gpt-4.1')) {
|
||||
return '$0.002/$0.008 per 1K tokens'
|
||||
} else if (model.includes('gpt-5-nano')) {
|
||||
return '$0.00005/$0.0004 per 1K tokens'
|
||||
} else if (model.includes('gpt-5-mini')) {
|
||||
return '$0.00025/$0.002 per 1K tokens'
|
||||
} else if (model.includes('gpt-5')) {
|
||||
return '$0.00125/$0.01 per 1K tokens'
|
||||
}
|
||||
return 'Token-based'
|
||||
}
|
||||
@@ -1358,6 +1409,7 @@ export const useNodePricing = () => {
|
||||
KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],
|
||||
KlingSingleImageVideoEffectNode: ['effect_scene'],
|
||||
KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],
|
||||
MinimaxHailuoVideoNode: ['resolution', 'duration'],
|
||||
OpenAIDalle3: ['size', 'quality'],
|
||||
OpenAIDalle2: ['size', 'n'],
|
||||
OpenAIGPTImage1: ['quality', 'n'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { groupBy } from 'lodash'
|
||||
import { groupBy } from 'es-toolkit/compat'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySidebarTab.vue'
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const AiModelIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/ai-model'))
|
||||
)
|
||||
|
||||
export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
return {
|
||||
id: 'model-library',
|
||||
icon: 'pi pi-box',
|
||||
icon: AiModelIcon,
|
||||
title: 'sideToolbar.modelLibrary',
|
||||
tooltip: 'sideToolbar.modelLibrary',
|
||||
label: 'sideToolbar.labels.models',
|
||||
component: markRaw(ModelLibrarySidebarTab),
|
||||
type: 'vue',
|
||||
iconBadge: () => {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import NodeLibrarySidebarTab from '@/components/sidebar/tabs/NodeLibrarySidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
const NodeIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/node'))
|
||||
)
|
||||
|
||||
export const useNodeLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
return {
|
||||
id: 'node-library',
|
||||
icon: 'pi pi-book',
|
||||
icon: NodeIcon,
|
||||
title: 'sideToolbar.nodeLibrary',
|
||||
tooltip: 'sideToolbar.nodeLibrary',
|
||||
label: 'sideToolbar.labels.nodes',
|
||||
component: markRaw(NodeLibrarySidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const useQueueSidebarTab = (): SidebarTabExtension => {
|
||||
},
|
||||
title: 'sideToolbar.queue',
|
||||
tooltip: 'sideToolbar.queue',
|
||||
label: 'sideToolbar.labels.queue',
|
||||
component: markRaw(QueueSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { markRaw } from 'vue'
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
const WorkflowIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/workflow'))
|
||||
)
|
||||
|
||||
export const useWorkflowsSidebarTab = (): SidebarTabExtension => {
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
return {
|
||||
id: 'workflows',
|
||||
icon: 'pi pi-folder-open',
|
||||
icon: WorkflowIcon,
|
||||
iconBadge: () => {
|
||||
if (
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') !== 'Sidebar'
|
||||
@@ -22,6 +26,7 @@ export const useWorkflowsSidebarTab = (): SidebarTabExtension => {
|
||||
},
|
||||
title: 'sideToolbar.workflows',
|
||||
tooltip: 'sideToolbar.workflows',
|
||||
label: 'sideToolbar.labels.workflows',
|
||||
component: markRaw(WorkflowsSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
|
||||
@@ -21,8 +21,11 @@ import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { type ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
@@ -276,6 +279,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
id: 'Comfy.Canvas.FitView',
|
||||
icon: 'pi pi-expand',
|
||||
label: 'Fit view to selected nodes',
|
||||
menubarLabel: 'Zoom to fit',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
if (app.canvas.empty) {
|
||||
@@ -301,6 +305,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
id: 'Comfy.Canvas.ToggleLinkVisibility',
|
||||
icon: 'pi pi-eye',
|
||||
label: 'Canvas Toggle Link Visibility',
|
||||
menubarLabel: 'Node Links',
|
||||
versionAdded: '1.3.6',
|
||||
|
||||
function: (() => {
|
||||
@@ -322,12 +327,15 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
)
|
||||
}
|
||||
}
|
||||
})()
|
||||
})(),
|
||||
active: () =>
|
||||
useSettingStore().get('Comfy.LinkRenderMode') !== LiteGraph.HIDDEN_LINK
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleMinimap',
|
||||
icon: 'pi pi-map',
|
||||
label: 'Canvas Toggle Minimap',
|
||||
menubarLabel: 'Minimap',
|
||||
versionAdded: '1.24.1',
|
||||
function: async () => {
|
||||
const settingStore = useSettingStore()
|
||||
@@ -335,7 +343,8 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
'Comfy.Minimap.Visible',
|
||||
!settingStore.get('Comfy.Minimap.Visible')
|
||||
)
|
||||
}
|
||||
},
|
||||
active: () => useSettingStore().get('Comfy.Minimap.Visible')
|
||||
},
|
||||
{
|
||||
id: 'Comfy.QueuePrompt',
|
||||
@@ -539,21 +548,25 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
id: 'Workspace.ToggleBottomPanel',
|
||||
icon: 'pi pi-list',
|
||||
label: 'Toggle Bottom Panel',
|
||||
menubarLabel: 'Bottom Panel',
|
||||
versionAdded: '1.3.22',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
bottomPanelStore.toggleBottomPanel()
|
||||
}
|
||||
},
|
||||
active: () => bottomPanelStore.bottomPanelVisible
|
||||
},
|
||||
{
|
||||
id: 'Workspace.ToggleFocusMode',
|
||||
icon: 'pi pi-eye',
|
||||
label: 'Toggle Focus Mode',
|
||||
menubarLabel: 'Focus Mode',
|
||||
versionAdded: '1.3.27',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
useWorkspaceStore().toggleFocusMode()
|
||||
}
|
||||
},
|
||||
active: () => useWorkspaceStore().focusMode
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.FitGroupToContents',
|
||||
@@ -794,8 +807,53 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
const { node } = res
|
||||
canvas.select(node)
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.UnpackSubgraph',
|
||||
icon: 'pi pi-sitemap',
|
||||
label: 'Unpack the selected Subgraph',
|
||||
versionAdded: '1.20.1',
|
||||
category: 'essentials' as const,
|
||||
function: () => {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const graph = canvas.subgraph ?? canvas.graph
|
||||
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
|
||||
|
||||
const subgraphNode = app.canvas.selectedItems.values().next().value
|
||||
useNodeOutputStore().revokeSubgraphPreviews(subgraphNode)
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.OpenManagerDialog',
|
||||
icon: 'mdi mdi-puzzle-outline',
|
||||
label: 'Manager',
|
||||
function: () => {
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleHelpCenter',
|
||||
icon: 'pi pi-question-circle',
|
||||
label: 'Help Center',
|
||||
function: () => {
|
||||
useHelpCenterStore().toggle()
|
||||
},
|
||||
active: () => useHelpCenterStore().isVisible
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleCanvasInfo',
|
||||
icon: 'pi pi-info-circle',
|
||||
label: 'Canvas Performance',
|
||||
function: async () => {
|
||||
const settingStore = useSettingStore()
|
||||
const currentValue = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
await settingStore.set('Comfy.Graph.CanvasInfo', !currentValue)
|
||||
},
|
||||
active: () => useSettingStore().get('Comfy.Graph.CanvasInfo')
|
||||
},
|
||||
{
|
||||
id: 'Workspace.ToggleBottomPanel.Shortcuts',
|
||||
icon: 'pi pi-key',
|
||||
@@ -805,6 +863,21 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
bottomPanelStore.togglePanel('shortcuts')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ExitSubgraph',
|
||||
icon: 'pi pi-arrow-up',
|
||||
label: 'Exit Subgraph',
|
||||
versionAdded: '1.20.1',
|
||||
function: () => {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
if (!canvas.graph) return
|
||||
|
||||
canvas.setGraph(
|
||||
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export const useLitegraphSettings = () => {
|
||||
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
if (canvasStore.canvas) {
|
||||
canvasStore.canvas.show_info = canvasInfoEnabled
|
||||
canvasStore.canvas.draw(false, true)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
376
src/composables/useLoad3dViewer.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { ref, toRaw, watch } from 'vue'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import {
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
interface Load3dViewerState {
|
||||
backgroundColor: string
|
||||
showGrid: boolean
|
||||
cameraType: CameraType
|
||||
fov: number
|
||||
lightIntensity: number
|
||||
cameraState: any
|
||||
backgroundImage: string
|
||||
upDirection: UpDirection
|
||||
materialMode: MaterialMode
|
||||
edgeThreshold: number
|
||||
}
|
||||
|
||||
export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
const backgroundColor = ref('')
|
||||
const showGrid = ref(true)
|
||||
const cameraType = ref<CameraType>('perspective')
|
||||
const fov = ref(75)
|
||||
const lightIntensity = ref(1)
|
||||
const backgroundImage = ref('')
|
||||
const hasBackgroundImage = ref(false)
|
||||
const upDirection = ref<UpDirection>('original')
|
||||
const materialMode = ref<MaterialMode>('original')
|
||||
const edgeThreshold = ref(85)
|
||||
const needApplyChanges = ref(true)
|
||||
|
||||
let load3d: Load3d | null = null
|
||||
let sourceLoad3d: Load3d | null = null
|
||||
|
||||
const initialState = ref<Load3dViewerState>({
|
||||
backgroundColor: '#282828',
|
||||
showGrid: true,
|
||||
cameraType: 'perspective',
|
||||
fov: 75,
|
||||
lightIntensity: 1,
|
||||
cameraState: null,
|
||||
backgroundImage: '',
|
||||
upDirection: 'original',
|
||||
materialMode: 'original',
|
||||
edgeThreshold: 85
|
||||
})
|
||||
|
||||
watch(backgroundColor, (newColor) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setBackgroundColor(newColor)
|
||||
} catch (error) {
|
||||
console.error('Error updating background color:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateBackgroundColor', { color: newColor })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(showGrid, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.toggleGrid(newValue)
|
||||
} catch (error) {
|
||||
console.error('Error toggling grid:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToToggleGrid', { show: newValue ? 'on' : 'off' })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(cameraType, (newCameraType) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.toggleCamera(newCameraType)
|
||||
} catch (error) {
|
||||
console.error('Error toggling camera:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToToggleCamera', { camera: newCameraType })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(fov, (newFov) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setFOV(Number(newFov))
|
||||
} catch (error) {
|
||||
console.error('Error updating FOV:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateFOV', { fov: newFov })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(lightIntensity, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setLightIntensity(Number(newValue))
|
||||
} catch (error) {
|
||||
console.error('Error updating light intensity:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateLightIntensity', { intensity: newValue })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(backgroundImage, async (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
await load3d.setBackgroundImage(newValue)
|
||||
hasBackgroundImage.value = !!newValue
|
||||
} catch (error) {
|
||||
console.error('Error updating background image:', error)
|
||||
useToastStore().addAlert(t('toastMessages.failedToUpdateBackgroundImage'))
|
||||
}
|
||||
})
|
||||
|
||||
watch(upDirection, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setUpDirection(newValue)
|
||||
} catch (error) {
|
||||
console.error('Error updating up direction:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateUpDirection', { direction: newValue })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(materialMode, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setMaterialMode(newValue)
|
||||
} catch (error) {
|
||||
console.error('Error updating material mode:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateMaterialMode', { mode: newValue })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(edgeThreshold, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setEdgeThreshold(Number(newValue))
|
||||
} catch (error) {
|
||||
console.error('Error updating edge threshold:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateEdgeThreshold', { threshold: newValue })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const initializeViewer = async (
|
||||
containerRef: HTMLElement,
|
||||
source: Load3d
|
||||
) => {
|
||||
if (!containerRef) return
|
||||
|
||||
sourceLoad3d = source
|
||||
|
||||
try {
|
||||
load3d = new Load3d(containerRef, {
|
||||
node: node,
|
||||
disablePreview: true,
|
||||
isViewerMode: true
|
||||
})
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, load3d)
|
||||
|
||||
const sourceCameraType = source.getCurrentCameraType()
|
||||
const sourceCameraState = source.getCameraState()
|
||||
|
||||
cameraType.value = sourceCameraType
|
||||
backgroundColor.value = source.sceneManager.currentBackgroundColor
|
||||
showGrid.value = source.sceneManager.gridHelper.visible
|
||||
lightIntensity.value = (node.properties['Light Intensity'] as number) || 1
|
||||
|
||||
const backgroundInfo = source.sceneManager.getCurrentBackgroundInfo()
|
||||
if (
|
||||
backgroundInfo.type === 'image' &&
|
||||
node.properties['Background Image']
|
||||
) {
|
||||
backgroundImage.value = node.properties['Background Image'] as string
|
||||
hasBackgroundImage.value = true
|
||||
} else {
|
||||
backgroundImage.value = ''
|
||||
hasBackgroundImage.value = false
|
||||
}
|
||||
|
||||
if (sourceCameraType === 'perspective') {
|
||||
fov.value = source.cameraManager.perspectiveCamera.fov
|
||||
}
|
||||
|
||||
upDirection.value = source.modelManager.currentUpDirection
|
||||
materialMode.value = source.modelManager.materialMode
|
||||
edgeThreshold.value = (node.properties['Edge Threshold'] as number) || 85
|
||||
|
||||
initialState.value = {
|
||||
backgroundColor: backgroundColor.value,
|
||||
showGrid: showGrid.value,
|
||||
cameraType: cameraType.value,
|
||||
fov: fov.value,
|
||||
lightIntensity: lightIntensity.value,
|
||||
cameraState: sourceCameraState,
|
||||
backgroundImage: backgroundImage.value,
|
||||
upDirection: upDirection.value,
|
||||
materialMode: materialMode.value,
|
||||
edgeThreshold: edgeThreshold.value
|
||||
}
|
||||
|
||||
const width = node.widgets?.find((w) => w.name === 'width')
|
||||
const height = node.widgets?.find((w) => w.name === 'height')
|
||||
|
||||
if (width && height) {
|
||||
load3d.setTargetSize(
|
||||
toRaw(width).value as number,
|
||||
toRaw(height).value as number
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing Load3d viewer:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToInitializeLoad3dViewer')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const exportModel = async (format: string) => {
|
||||
if (!load3d) return
|
||||
|
||||
try {
|
||||
await load3d.exportModel(format)
|
||||
} catch (error) {
|
||||
console.error('Error exporting model:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToExportModel', { format: format.toUpperCase() })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
load3d?.handleResize()
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
load3d?.updateStatusMouseOnViewer(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
load3d?.updateStatusMouseOnViewer(false)
|
||||
}
|
||||
|
||||
const restoreInitialState = () => {
|
||||
const nodeValue = node
|
||||
|
||||
needApplyChanges.value = false
|
||||
|
||||
if (nodeValue.properties) {
|
||||
nodeValue.properties['Background Color'] =
|
||||
initialState.value.backgroundColor
|
||||
nodeValue.properties['Show Grid'] = initialState.value.showGrid
|
||||
nodeValue.properties['Camera Type'] = initialState.value.cameraType
|
||||
nodeValue.properties['FOV'] = initialState.value.fov
|
||||
nodeValue.properties['Light Intensity'] =
|
||||
initialState.value.lightIntensity
|
||||
nodeValue.properties['Camera Info'] = initialState.value.cameraState
|
||||
nodeValue.properties['Background Image'] =
|
||||
initialState.value.backgroundImage
|
||||
}
|
||||
}
|
||||
|
||||
const applyChanges = async () => {
|
||||
if (!sourceLoad3d || !load3d) return false
|
||||
|
||||
const viewerCameraState = load3d.getCameraState()
|
||||
const nodeValue = node
|
||||
|
||||
if (nodeValue.properties) {
|
||||
nodeValue.properties['Background Color'] = backgroundColor.value
|
||||
nodeValue.properties['Show Grid'] = showGrid.value
|
||||
nodeValue.properties['Camera Type'] = cameraType.value
|
||||
nodeValue.properties['FOV'] = fov.value
|
||||
nodeValue.properties['Light Intensity'] = lightIntensity.value
|
||||
nodeValue.properties['Camera Info'] = viewerCameraState
|
||||
nodeValue.properties['Background Image'] = backgroundImage.value
|
||||
}
|
||||
|
||||
await useLoad3dService().copyLoad3dState(load3d, sourceLoad3d)
|
||||
|
||||
if (backgroundImage.value) {
|
||||
await sourceLoad3d.setBackgroundImage(backgroundImage.value)
|
||||
}
|
||||
|
||||
sourceLoad3d.forceRender()
|
||||
|
||||
if (nodeValue.graph) {
|
||||
nodeValue.graph.setDirtyCanvas(true, true)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const refreshViewport = () => {
|
||||
useLoad3dService().handleViewportRefresh(load3d)
|
||||
}
|
||||
|
||||
const handleBackgroundImageUpdate = async (file: File | null) => {
|
||||
if (!file) {
|
||||
backgroundImage.value = ''
|
||||
hasBackgroundImage.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resourceFolder =
|
||||
(node.properties['Resource Folder'] as string) || ''
|
||||
const subfolder = resourceFolder.trim()
|
||||
? `3d/${resourceFolder.trim()}`
|
||||
: '3d'
|
||||
|
||||
const uploadPath = await Load3dUtils.uploadFile(file, subfolder)
|
||||
|
||||
if (uploadPath) {
|
||||
backgroundImage.value = uploadPath
|
||||
hasBackgroundImage.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading background image:', error)
|
||||
useToastStore().addAlert(t('toastMessages.failedToUploadBackgroundImage'))
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
load3d?.remove()
|
||||
load3d = null
|
||||
sourceLoad3d = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
backgroundColor,
|
||||
showGrid,
|
||||
cameraType,
|
||||
fov,
|
||||
lightIntensity,
|
||||
backgroundImage,
|
||||
hasBackgroundImage,
|
||||
upDirection,
|
||||
materialMode,
|
||||
edgeThreshold,
|
||||
needApplyChanges,
|
||||
|
||||
// Methods
|
||||
initializeViewer,
|
||||
exportModel,
|
||||
handleResize,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
restoreInitialState,
|
||||
applyChanges,
|
||||
refreshViewport,
|
||||
handleBackgroundImageUpdate,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,14 @@ import { useRafFn, useThrottleFn } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
interface GraphCallbacks {
|
||||
onNodeAdded?: (node: LGraphNode) => void
|
||||
@@ -16,9 +17,17 @@ interface GraphCallbacks {
|
||||
onConnectionChange?: (node: LGraphNode) => void
|
||||
}
|
||||
|
||||
export type MinimapOptionKey =
|
||||
| 'Comfy.Minimap.NodeColors'
|
||||
| 'Comfy.Minimap.ShowLinks'
|
||||
| 'Comfy.Minimap.ShowGroups'
|
||||
| 'Comfy.Minimap.RenderBypassState'
|
||||
| 'Comfy.Minimap.RenderErrorState'
|
||||
|
||||
export function useMinimap() {
|
||||
const settingStore = useSettingStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
@@ -27,6 +36,27 @@ export function useMinimap() {
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
const nodeColors = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.NodeColors')
|
||||
)
|
||||
const showLinks = computed(() => settingStore.get('Comfy.Minimap.ShowLinks'))
|
||||
const showGroups = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.ShowGroups')
|
||||
)
|
||||
const renderBypass = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderBypassState')
|
||||
)
|
||||
const renderError = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderErrorState')
|
||||
)
|
||||
|
||||
const updateOption = async (key: MinimapOptionKey, value: boolean) => {
|
||||
await settingStore.set(key, value)
|
||||
|
||||
needsFullRedraw.value = true
|
||||
updateMinimap()
|
||||
}
|
||||
|
||||
const initialized = ref(false)
|
||||
const bounds = ref({
|
||||
minX: 0,
|
||||
@@ -63,10 +93,22 @@ export function useMinimap() {
|
||||
const nodeColor = computed(
|
||||
() => (isLightTheme.value ? '#3DA8E099' : '#0B8CE999') // lighter blue for light theme
|
||||
)
|
||||
const nodeColorDefault = computed(
|
||||
() => (isLightTheme.value ? '#D9D9D9' : '#353535') // this is the default node color when using nodeColors setting
|
||||
)
|
||||
const linkColor = computed(
|
||||
() => (isLightTheme.value ? '#FFB347' : '#F99614') // lighter orange for light theme
|
||||
() => (isLightTheme.value ? '#616161' : '#B3B3B3') // lighter orange for light theme
|
||||
)
|
||||
const slotColor = computed(() => linkColor.value)
|
||||
const groupColor = computed(() =>
|
||||
isLightTheme.value ? '#A2D3EC' : '#1F547A'
|
||||
)
|
||||
const groupColorDefault = computed(
|
||||
() => (isLightTheme.value ? '#283640' : '#B3C1CB') // this is the default group color when using nodeColors setting
|
||||
)
|
||||
const bypassColor = computed(() =>
|
||||
isLightTheme.value ? '#DBDBDB' : '#4B184B'
|
||||
)
|
||||
|
||||
const containerRect = ref({
|
||||
left: 0,
|
||||
@@ -106,7 +148,11 @@ export function useMinimap() {
|
||||
}
|
||||
|
||||
const canvas = computed(() => canvasStore.canvas)
|
||||
const graph = ref(app.canvas?.graph)
|
||||
const graph = computed(() => {
|
||||
// If we're in a subgraph, use that; otherwise use the canvas graph
|
||||
const activeSubgraph = workflowStore.activeSubgraph
|
||||
return activeSubgraph || canvas.value?.graph
|
||||
})
|
||||
|
||||
const containerStyles = computed(() => ({
|
||||
width: `${width}px`,
|
||||
@@ -116,6 +162,14 @@ export function useMinimap() {
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
const panelStyles = computed(() => ({
|
||||
width: `210px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: isLightTheme.value ? '#FAF9F5' : '#15161C',
|
||||
border: `1px solid ${isLightTheme.value ? '#ccc' : '#333'}`,
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
const viewportStyles = computed(() => ({
|
||||
transform: `translate(${viewportTransform.value.x}px, ${viewportTransform.value.y}px)`,
|
||||
width: `${viewportTransform.value.width}px`,
|
||||
@@ -189,6 +243,35 @@ export function useMinimap() {
|
||||
return Math.min(scaleX, scaleY) * 0.9
|
||||
}
|
||||
|
||||
const renderGroups = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
) => {
|
||||
const g = graph.value
|
||||
if (!g || !g._groups || g._groups.length === 0) return
|
||||
|
||||
for (const group of g._groups) {
|
||||
const x = (group.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||
const y = (group.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||
const w = group.size[0] * scale.value
|
||||
const h = group.size[1] * scale.value
|
||||
|
||||
let color = groupColor.value
|
||||
|
||||
if (nodeColors.value) {
|
||||
color = group.color ?? groupColorDefault.value
|
||||
|
||||
if (isLightTheme.value) {
|
||||
color = adjustColor(color, { opacity: 0.5 })
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
const renderNodes = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
@@ -203,9 +286,29 @@ export function useMinimap() {
|
||||
const w = node.size[0] * scale.value
|
||||
const h = node.size[1] * scale.value
|
||||
|
||||
let color = nodeColor.value
|
||||
|
||||
if (renderBypass.value && node.mode === LGraphEventMode.BYPASS) {
|
||||
color = bypassColor.value
|
||||
} else if (nodeColors.value) {
|
||||
color = nodeColorDefault.value
|
||||
|
||||
if (node.bgcolor) {
|
||||
color = isLightTheme.value
|
||||
? adjustColor(node.bgcolor, { lightness: 0.5 })
|
||||
: node.bgcolor
|
||||
}
|
||||
}
|
||||
|
||||
// Render solid node blocks
|
||||
ctx.fillStyle = nodeColor.value
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(x, y, w, h)
|
||||
|
||||
if (renderError.value && node.has_errors) {
|
||||
ctx.strokeStyle = '#FF0000'
|
||||
ctx.lineWidth = 0.3
|
||||
ctx.strokeRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,9 +321,9 @@ export function useMinimap() {
|
||||
if (!g) return
|
||||
|
||||
ctx.strokeStyle = linkColor.value
|
||||
ctx.lineWidth = 1.4
|
||||
ctx.lineWidth = 0.3
|
||||
|
||||
const slotRadius = 3.7 * Math.max(scale.value, 0.5) // Larger slots that scale
|
||||
const slotRadius = Math.max(scale.value, 0.5) // Larger slots that scale
|
||||
const connections: Array<{
|
||||
x1: number
|
||||
y1: number
|
||||
@@ -304,8 +407,15 @@ export function useMinimap() {
|
||||
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
if (showGroups.value) {
|
||||
renderGroups(ctx, offsetX, offsetY)
|
||||
}
|
||||
|
||||
if (showLinks.value) {
|
||||
renderConnections(ctx, offsetX, offsetY)
|
||||
}
|
||||
|
||||
renderNodes(ctx, offsetX, offsetY)
|
||||
renderConnections(ctx, offsetX, offsetY)
|
||||
|
||||
needsFullRedraw.value = false
|
||||
updateFlags.value.nodes = false
|
||||
@@ -522,7 +632,8 @@ export function useMinimap() {
|
||||
c.setDirty(true, true)
|
||||
}
|
||||
|
||||
let originalCallbacks: GraphCallbacks = {}
|
||||
// Map to store original callbacks per graph ID
|
||||
const originalCallbacksMap = new Map<string, GraphCallbacks>()
|
||||
|
||||
const handleGraphChanged = useThrottleFn(() => {
|
||||
needsFullRedraw.value = true
|
||||
@@ -536,11 +647,18 @@ export function useMinimap() {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
originalCallbacks = {
|
||||
// Check if we've already wrapped this graph's callbacks
|
||||
if (originalCallbacksMap.has(g.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store the original callbacks for this graph
|
||||
const originalCallbacks: GraphCallbacks = {
|
||||
onNodeAdded: g.onNodeAdded,
|
||||
onNodeRemoved: g.onNodeRemoved,
|
||||
onConnectionChange: g.onConnectionChange
|
||||
}
|
||||
originalCallbacksMap.set(g.id, originalCallbacks)
|
||||
|
||||
g.onNodeAdded = function (node) {
|
||||
originalCallbacks.onNodeAdded?.call(this, node)
|
||||
@@ -565,15 +683,18 @@ export function useMinimap() {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
if (originalCallbacks.onNodeAdded !== undefined) {
|
||||
g.onNodeAdded = originalCallbacks.onNodeAdded
|
||||
}
|
||||
if (originalCallbacks.onNodeRemoved !== undefined) {
|
||||
g.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||
}
|
||||
if (originalCallbacks.onConnectionChange !== undefined) {
|
||||
g.onConnectionChange = originalCallbacks.onConnectionChange
|
||||
const originalCallbacks = originalCallbacksMap.get(g.id)
|
||||
if (!originalCallbacks) {
|
||||
throw new Error(
|
||||
'Attempted to cleanup event listeners for graph that was never set up'
|
||||
)
|
||||
}
|
||||
|
||||
g.onNodeAdded = originalCallbacks.onNodeAdded
|
||||
g.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||
g.onConnectionChange = originalCallbacks.onConnectionChange
|
||||
|
||||
originalCallbacksMap.delete(g.id)
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
@@ -646,6 +767,19 @@ export function useMinimap() {
|
||||
{ immediate: true, flush: 'post' }
|
||||
)
|
||||
|
||||
// Watch for graph changes (e.g., when navigating to/from subgraphs)
|
||||
watch(graph, (newGraph, oldGraph) => {
|
||||
if (newGraph && newGraph !== oldGraph) {
|
||||
cleanupEventListeners()
|
||||
setupEventListeners()
|
||||
needsFullRedraw.value = true
|
||||
updateFlags.value.bounds = true
|
||||
updateFlags.value.nodes = true
|
||||
updateFlags.value.connections = true
|
||||
updateMinimap()
|
||||
}
|
||||
})
|
||||
|
||||
watch(visible, async (isVisible) => {
|
||||
if (isVisible) {
|
||||
if (containerRef.value) {
|
||||
@@ -690,9 +824,16 @@ export function useMinimap() {
|
||||
canvasRef,
|
||||
containerStyles,
|
||||
viewportStyles,
|
||||
panelStyles,
|
||||
width,
|
||||
height,
|
||||
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
|
||||
init,
|
||||
destroy,
|
||||
toggle,
|
||||
@@ -701,6 +842,7 @@ export function useMinimap() {
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handleWheel,
|
||||
setMinimapRef
|
||||
setMinimapRef,
|
||||
updateOption
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { orderBy } from 'lodash'
|
||||
import { orderBy } from 'es-toolkit/compat'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { DEFAULT_PAGE_SIZE } from '@/constants/searchConstants'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tryOnScopeDispose } from '@vueuse/core'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
@@ -88,6 +89,11 @@ export function useWorkflowPersistence() {
|
||||
)
|
||||
api.addEventListener('graphChanged', persistCurrentWorkflow)
|
||||
|
||||
// Clean up event listener when component unmounts
|
||||
tryOnScopeDispose(() => {
|
||||
api.removeEventListener('graphChanged', persistCurrentWorkflow)
|
||||
})
|
||||
|
||||
// Restore workflow tabs states
|
||||
const openWorkflows = computed(() => workflowStore.openWorkflows)
|
||||
const activeWorkflow = computed(() => workflowStore.activeWorkflow)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
@@ -190,5 +190,11 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
key: 'k'
|
||||
},
|
||||
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Escape'
|
||||
},
|
||||
commandId: 'Comfy.Graph.ExitSubgraph'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export const CORE_MENU_COMMANDS = [
|
||||
[['Workflow'], ['Comfy.NewBlankWorkflow']],
|
||||
[['Workflow'], ['Comfy.OpenWorkflow', 'Comfy.BrowseTemplates']],
|
||||
[[], ['Comfy.NewBlankWorkflow']],
|
||||
[[], []], // Separator after New
|
||||
[['File'], ['Comfy.OpenWorkflow']],
|
||||
[
|
||||
['Workflow'],
|
||||
['File'],
|
||||
[
|
||||
'Comfy.SaveWorkflow',
|
||||
'Comfy.SaveWorkflowAs',
|
||||
@@ -11,8 +12,6 @@ export const CORE_MENU_COMMANDS = [
|
||||
]
|
||||
],
|
||||
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
|
||||
[['Edit'], ['Comfy.RefreshNodeDefinitions']],
|
||||
[['Edit'], ['Comfy.ClearWorkflow']],
|
||||
[['Edit'], ['Comfy.OpenClipspace']],
|
||||
[
|
||||
['Help'],
|
||||
|
||||
@@ -300,7 +300,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
{ value: 'ja', text: '日本語' },
|
||||
{ value: 'ko', text: '한국어' },
|
||||
{ value: 'fr', text: 'Français' },
|
||||
{ value: 'es', text: 'Español' }
|
||||
{ value: 'es', text: 'Español' },
|
||||
{ value: 'ar', text: 'عربي' }
|
||||
],
|
||||
defaultValue: () => navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
@@ -771,7 +772,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
{
|
||||
id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',
|
||||
name: 'Low quality rendering zoom threshold',
|
||||
tooltip: 'Render low quality shapes when zoomed out',
|
||||
tooltip:
|
||||
'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in. Performance mode simplifies rendering by hiding text labels, shadows, and details.',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0.1,
|
||||
@@ -830,6 +832,41 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: true,
|
||||
versionAdded: '1.25.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.NodeColors',
|
||||
name: 'Display node with its original color on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.ShowLinks',
|
||||
name: 'Display links on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.ShowGroups',
|
||||
name: 'Display node groups on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.RenderBypassState',
|
||||
name: 'Render bypass state on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.RenderErrorState',
|
||||
name: 'Render error state on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.AutoSaveDelay',
|
||||
name: 'Auto Save Delay (ms)',
|
||||
|
||||