Compare commits
204 Commits
fix-queue-
...
v1.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8f62cd963 | ||
|
|
9900e46b95 | ||
|
|
c433b38ba8 | ||
|
|
0b501d7a1c | ||
|
|
0e197261b0 | ||
|
|
89bf5c182d | ||
|
|
e80c75482e | ||
|
|
569a131624 | ||
|
|
b1d02c6a7b | ||
|
|
abeafd5625 | ||
|
|
0906d7873a | ||
|
|
0ce3f1ecb0 | ||
|
|
ec6919c5ab | ||
|
|
10de690ccf | ||
|
|
cb3e4b5ed7 | ||
|
|
2b4ac582d4 | ||
|
|
7f60811ebf | ||
|
|
90e94df1e4 | ||
|
|
5b72fc7fdc | ||
|
|
601b739f75 | ||
|
|
a441908db3 | ||
|
|
a376503a7b | ||
|
|
69a4a17848 | ||
|
|
22f9ad31dc | ||
|
|
2a02159355 | ||
|
|
9258098e0c | ||
|
|
fa48fe587d | ||
|
|
7b64bfa173 | ||
|
|
59b62eadf1 | ||
|
|
98912efeb7 | ||
|
|
c7303ea465 | ||
|
|
618b451f1e | ||
|
|
a99f63784d | ||
|
|
6f7c7f3c61 | ||
|
|
4f049b66a2 | ||
|
|
458ff350c8 | ||
|
|
fc8c10bca4 | ||
|
|
28ab86cd4a | ||
|
|
efb6f8afe3 | ||
|
|
a2549f23c7 | ||
|
|
7877c96ffe | ||
|
|
4997d13219 | ||
|
|
c5135afd29 | ||
|
|
8663340067 | ||
|
|
a1ffc521a4 | ||
|
|
f7dce3116a | ||
|
|
f23530afcf | ||
|
|
c5f77de1bf | ||
|
|
c60645d9f4 | ||
|
|
ebbff358a0 | ||
|
|
a39c04d56e | ||
|
|
9e61d1509f | ||
|
|
0c511d08e6 | ||
|
|
929e3a5733 | ||
|
|
75c98d00fc | ||
|
|
7bce5ac4d0 | ||
|
|
e7733d94f9 | ||
|
|
20a3a95df2 | ||
|
|
d2bbf41fb5 | ||
|
|
97ccc6a854 | ||
|
|
b48619878f | ||
|
|
ac6130a556 | ||
|
|
86797d2492 | ||
|
|
112a693f3d | ||
|
|
57701f6145 | ||
|
|
c2145b6f45 | ||
|
|
0b91d53c9f | ||
|
|
b4ccaf4fec | ||
|
|
2d1b2cb1cc | ||
|
|
aa04ab78c1 | ||
|
|
1671437fb3 | ||
|
|
9d49cb0e4b | ||
|
|
43548785b5 | ||
|
|
b6038128cb | ||
|
|
a76159e9a0 | ||
|
|
a0ff78dbdb | ||
|
|
059dfcbebb | ||
|
|
c17d44638e | ||
|
|
0e385c4262 | ||
|
|
da98b1c4cc | ||
|
|
26d2e5de31 | ||
|
|
b0e37036d5 | ||
|
|
b7ddb47d2a | ||
|
|
6eb5ee99d8 | ||
|
|
6409e17d4a | ||
|
|
a6549fb41e | ||
|
|
06d5064b7c | ||
|
|
fd621f485e | ||
|
|
517ae56763 | ||
|
|
ae26390776 | ||
|
|
aa68422e0f | ||
|
|
dac2a2ec86 | ||
|
|
5cee4d828f | ||
|
|
d6247d69ce | ||
|
|
e2fa1e65d1 | ||
|
|
55c04b5533 | ||
|
|
540e785424 | ||
|
|
30a92d40eb | ||
|
|
679405e0a2 | ||
|
|
6cdc524174 | ||
|
|
80a42ebf10 | ||
|
|
c45f03f5e7 | ||
|
|
36c2604639 | ||
|
|
73f50e7e0b | ||
|
|
418fddd669 | ||
|
|
e3cda0e749 | ||
|
|
30465f17e0 | ||
|
|
5206939c78 | ||
|
|
73396784a8 | ||
|
|
3745d8d791 | ||
|
|
735153886f | ||
|
|
d04cc4e272 | ||
|
|
473aa120eb | ||
|
|
7986aebf27 | ||
|
|
2caa87d35d | ||
|
|
dabcd4741b | ||
|
|
e893f3ed03 | ||
|
|
1856479de9 | ||
|
|
fc69129a2f | ||
|
|
d953b8fa46 | ||
|
|
1847db7a47 | ||
|
|
9cbcda20a7 | ||
|
|
0fe0aea242 | ||
|
|
08ae36818a | ||
|
|
da6b4d2872 | ||
|
|
8e293b41f5 | ||
|
|
d2771a7a1d | ||
|
|
13869da300 | ||
|
|
6ba53f3af1 | ||
|
|
b23cebcba4 | ||
|
|
30dfe76577 | ||
|
|
c1f0cfe366 | ||
|
|
d1279fa474 | ||
|
|
a56462fc7c | ||
|
|
646bcf595b | ||
|
|
4796677a0a | ||
|
|
01ac9d9336 | ||
|
|
89a9288154 | ||
|
|
17b7ef18d6 | ||
|
|
1afc584393 | ||
|
|
9d71cdf8ef | ||
|
|
1c7f3e865a | ||
|
|
9ef40189f9 | ||
|
|
5c6eecd660 | ||
|
|
9b07993e1a | ||
|
|
c35d29f31c | ||
|
|
e50d7c5eef | ||
|
|
7c2cce40de | ||
|
|
2017b9016b | ||
|
|
0bf30e7621 | ||
|
|
7f5b685c9f | ||
|
|
ec824579d6 | ||
|
|
9e565154a9 | ||
|
|
541335bb31 | ||
|
|
814c4b8ef0 | ||
|
|
df3fff5dbb | ||
|
|
b0085114d7 | ||
|
|
7f9c70386f | ||
|
|
578870d345 | ||
|
|
05fab91bda | ||
|
|
5191e11650 | ||
|
|
92079a653e | ||
|
|
a7d14eb815 | ||
|
|
9aea6eae70 | ||
|
|
88a42172c5 | ||
|
|
e79013dcfe | ||
|
|
08f3370828 | ||
|
|
c4d3c672ad | ||
|
|
39eaa2e850 | ||
|
|
2d022e4e49 | ||
|
|
1ac6d6529f | ||
|
|
86fec820ac | ||
|
|
030d5845db | ||
|
|
dd1c878fdf | ||
|
|
3942603a38 | ||
|
|
244578db96 | ||
|
|
6b6edfde9f | ||
|
|
c54b675a48 | ||
|
|
b7008dfc5c | ||
|
|
d0ad4af51c | ||
|
|
4a182014e1 | ||
|
|
46cd522384 | ||
|
|
c977667a15 | ||
|
|
d531bc34c4 | ||
|
|
adfbec2744 | ||
|
|
23521559bf | ||
|
|
51f57aba17 | ||
|
|
97bab053df | ||
|
|
c1c5573e7f | ||
|
|
16d2a95760 | ||
|
|
f97b673481 | ||
|
|
c8d5a6f154 | ||
|
|
3708afaf21 | ||
|
|
43c23e526c | ||
|
|
a80eb84df1 | ||
|
|
f89898b3d0 | ||
|
|
af21142602 | ||
|
|
4b91860227 | ||
|
|
e53bafbca6 | ||
|
|
e01c8f06c7 | ||
|
|
c61ed4da37 | ||
|
|
4a4d6d070a | ||
|
|
4bedd873a1 | ||
|
|
f8bd910e63 |
127
.github/workflows/i18n-custom-nodes.yaml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: Update Locales for given custom node repository
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
owner:
|
||||
description: 'Owner of the repository to update locales for'
|
||||
required: true
|
||||
type: string
|
||||
repository:
|
||||
description: 'Repository to update locales for'
|
||||
required: true
|
||||
type: string
|
||||
fork_owner:
|
||||
description: 'Owner of the forked repository'
|
||||
required: false
|
||||
type: string
|
||||
default: 'Comfy-Org'
|
||||
|
||||
jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout ComfyUI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'comfyanonymous/ComfyUI'
|
||||
path: 'ComfyUI'
|
||||
ref: master
|
||||
- name: Checkout ComfyUI_frontend
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_frontend'
|
||||
path: 'ComfyUI_frontend'
|
||||
- name: Checkout ComfyUI_devtools
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||
- name: Checkout custom node repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.owner }}/${{ inputs.repository }}
|
||||
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install ComfyUI requirements
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||
pip install -r requirements.txt
|
||||
pip install wait-for-it
|
||||
shell: bash
|
||||
working-directory: ComfyUI
|
||||
- name: Install custom node requirements
|
||||
run: |
|
||||
if [ -f "requirements.txt" ]; then
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
shell: bash
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
- name: Build & Install ComfyUI_frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
rm -rf ../ComfyUI/web/*
|
||||
mv dist/* ../ComfyUI/web/
|
||||
shell: bash
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start ComfyUI server
|
||||
run: |
|
||||
python main.py --cpu --multi-user &
|
||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||
working-directory: ComfyUI
|
||||
shell: bash
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: npm run dev:electron &
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Capture base i18n
|
||||
run: npx tsx scripts/diff-i18n capture
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update en.json
|
||||
run: npm run collect-i18n
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update translations
|
||||
run: npm run locale
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Diff base vs updated i18n
|
||||
run: npx tsx scripts/diff-i18n diff
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update i18n in custom node repository
|
||||
run: |
|
||||
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
|
||||
install -d "$LOCALE_DIR"
|
||||
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
|
||||
- name: Check and create fork of custom node repository
|
||||
run: |
|
||||
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
|
||||
echo "Fork failed - repository might already be forked"
|
||||
# Exit 0 to prevent the workflow from failing
|
||||
exit 0
|
||||
}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
- name: Commit and push changes
|
||||
run: |
|
||||
git checkout -b update-locales
|
||||
git add -A
|
||||
git commit -m "Update locales"
|
||||
git remote add org_fork git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git
|
||||
git push org_fork update-locales
|
||||
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales
|
||||
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
39
.github/workflows/i18n.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Update Locales
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main, master, dev* ]
|
||||
|
||||
jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
run: npm run dev:electron &
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update en.json
|
||||
run: npm run collect-i18n
|
||||
env:
|
||||
PLAYWRIGHT_TEST_URL: http://localhost:5173
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update translations
|
||||
# Pipe output so that it doesn't error out on stdout.clearLine
|
||||
run: npm run locale 2>&1 | cat
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Commit updated locales
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
git fetch origin ${{ github.head_ref }}
|
||||
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
|
||||
git add src/locales/
|
||||
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
working-directory: ComfyUI_frontend
|
||||
18
.github/workflows/release.yaml
vendored
@@ -41,3 +41,21 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
make_latest: "true"
|
||||
publish_types:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'Release')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
- run: npm run build:types
|
||||
- name: Publish package
|
||||
run: npm publish --access public
|
||||
working-directory: ./dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
4
.gitignore
vendored
@@ -39,4 +39,6 @@ browser_tests/*/*-win32.png
|
||||
|
||||
.env
|
||||
|
||||
dist.zip
|
||||
dist.zip
|
||||
|
||||
/temp/
|
||||
17
.i18nrc.cjs
Normal file
@@ -0,0 +1,17 @@
|
||||
// This file is intentionally kept in CommonJS format (.cjs)
|
||||
// to resolve compatibility issues with dependencies that require CommonJS.
|
||||
// Do not convert this file to ESModule format unless all dependencies support it.
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
modelName: 'gpt-4',
|
||||
splitToken: 1024,
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja', 'ko'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
`
|
||||
});
|
||||
17
CODEOWNERS
Normal file
@@ -0,0 +1,17 @@
|
||||
# Admins
|
||||
* @Comfy-Org/comfy_frontend_devs
|
||||
|
||||
# Maintainers
|
||||
*.md @Comfy-Org/comfy_maintainer
|
||||
/tests-ui/ @Comfy-Org/comfy_maintainer
|
||||
/browser_tests/ @Comfy-Org/comfy_maintainer
|
||||
/.env_example @Comfy-Org/comfy_maintainer
|
||||
|
||||
# Translations (AIGODLIKE team + shinshin86)
|
||||
/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer
|
||||
|
||||
# Load 3D extension
|
||||
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
|
||||
|
||||
# Mask Editor extension
|
||||
/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs
|
||||
103
README.md
@@ -52,16 +52,30 @@ pause
|
||||
|
||||
### Stable Release
|
||||
|
||||
Stable releases are published weekly in the ComfyUI main repository, aligned with ComfyUI backend's stable release schedule.
|
||||
|
||||
#### Feature Freeze
|
||||
|
||||
There will be a 2-day feature freeze before each stable release. During this period, no new major features will be merged.
|
||||
Stable releases are published bi-weekly in the ComfyUI main repository.
|
||||
|
||||
## Release Summary
|
||||
|
||||
### Major features
|
||||
|
||||
<details>
|
||||
<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
|
||||
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
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.4: New mask editor</summary>
|
||||
|
||||
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.22: Integrated server terminal</summary>
|
||||
|
||||
@@ -414,6 +428,7 @@ We will support custom icons later.
|
||||
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
|
||||
- [Litegraph](https://github.com/Comfy-Org/litegraph.js) for node editor
|
||||
- [zod](https://zod.dev/) for schema validation
|
||||
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
|
||||
|
||||
### Git pre-commit hooks
|
||||
|
||||
@@ -480,6 +495,84 @@ This repo is using litegraph package hosted on <https://github.com/Comfy-Org/lit
|
||||
|
||||
This will replace the litegraph package in this repo with the local litegraph repo.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
Our project supports multiple languages using `vue-i18n`. This allows users around the world to use the application in their preferred language.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
- en (English)
|
||||
- zh (中文)
|
||||
- ru (Русский)
|
||||
- ja (日本語)
|
||||
- ko (한국어)
|
||||
|
||||
### How to Add a New Language
|
||||
|
||||
We welcome the addition of new languages. You can add a new language by following these steps:
|
||||
|
||||
#### 1. Generate language files
|
||||
We use [lobe-i18n](https://github.com/lobehub/lobe-cli-toolbox/blob/master/packages/lobe-i18n/README.md) as our translation tool, which integrates with LLM for efficient localization.
|
||||
|
||||
Update the configuration file to include the new language(s) you wish to add:
|
||||
|
||||
|
||||
```javascript
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
entry: 'src/locales/en.json', // Base language file
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja'], // Add the new language(s) here
|
||||
});
|
||||
```
|
||||
|
||||
Set your OpenAI API Key by running the following command:
|
||||
|
||||
```sh
|
||||
npx lobe-i18n --option
|
||||
```
|
||||
|
||||
Once configured, generate the translation files with:
|
||||
|
||||
```sh
|
||||
npx lobe-i18n locale
|
||||
```
|
||||
|
||||
This will create the language files for the specified languages in the configuration.
|
||||
|
||||
#### 2. Update i18n Configuration
|
||||
|
||||
Import the newly generated locale file(s) in the `src/i18n.ts` file to include them in the application's i18n setup.
|
||||
|
||||
#### 3. Enable Selection of the New Language
|
||||
|
||||
Add the newly added language to the following item in `src/constants/coreSettings.ts`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Locale',
|
||||
name: 'Locale',
|
||||
type: 'combo',
|
||||
// Add the new language(s) here
|
||||
options: [
|
||||
{ value: 'en', text: 'English' },
|
||||
{ value: 'zh', text: '中文' },
|
||||
{ value: 'ru', text: 'Русский' },
|
||||
{ value: 'ja', text: '日本語' }
|
||||
],
|
||||
defaultValue: navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
```
|
||||
|
||||
This will make the new language selectable in the application's settings.
|
||||
|
||||
#### 4. Test the Translations
|
||||
|
||||
Start the development server, switch to the new language, and verify the translations.
|
||||
You can switch languages by opening the ComfyUI Settings and selecting from the `ComfyUI > Locale` dropdown box.
|
||||
|
||||
## Deploy
|
||||
|
||||
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.
|
||||
|
||||
49
browser_tests/assets/simple_slider.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "DevToolsSimpleSlider",
|
||||
"pos": [
|
||||
50,
|
||||
50
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"label": "FLOAT"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsSimpleSlider"
|
||||
},
|
||||
"widgets_values": [
|
||||
0.5
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -3,9 +3,6 @@ import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from './fixtures/ComfyPage'
|
||||
import type { useWorkspaceStore } from '../src/stores/workspaceStore'
|
||||
|
||||
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
|
||||
async function beforeChange(comfyPage: ComfyPage) {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
@@ -26,64 +23,41 @@ test.describe('Change Tracker', () => {
|
||||
})
|
||||
|
||||
test('Can undo multiple operations', async ({ comfyPage }) => {
|
||||
function isModified() {
|
||||
return comfyPage.page.evaluate(async () => {
|
||||
return !!(window['app'].extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow?.isModified
|
||||
})
|
||||
}
|
||||
|
||||
function getUndoQueueSize() {
|
||||
return comfyPage.page.evaluate(() => {
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.undoQueue.length
|
||||
})
|
||||
}
|
||||
|
||||
function getRedoQueueSize() {
|
||||
return comfyPage.page.evaluate(() => {
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.redoQueue.length
|
||||
})
|
||||
}
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
// Save, confirm no errors & workflow modified flag removed
|
||||
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(0)
|
||||
expect(await isModified()).toBe(false)
|
||||
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
await node.click('title')
|
||||
await node.click('collapse')
|
||||
await expect(node).toBeCollapsed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(1)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(2)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(1)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(1)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(1)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
expect(await isModified()).toBe(false)
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getRedoQueueSize()).toBe(2)
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
expect(await comfyPage.getRedoQueueSize()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -174,4 +148,20 @@ test.describe('Change Tracker', () => {
|
||||
await expect(node).toBePinned()
|
||||
await expect(node).toBeCollapsed()
|
||||
})
|
||||
|
||||
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].graph.extra.foo = 'bar'
|
||||
})
|
||||
// Click empty space to trigger a change detection.
|
||||
await comfyPage.clickEmptySpace()
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(1)
|
||||
})
|
||||
|
||||
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
await comfyPage.pan({ x: 10, y: 10 })
|
||||
expect(await comfyPage.getUndoQueueSize()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
@@ -17,8 +17,11 @@ import {
|
||||
import { Topbar } from './components/Topbar'
|
||||
import { NodeReference } from './utils/litegraphUtils'
|
||||
import type { Position, Size } from './types'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
|
||||
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
||||
|
||||
class ComfyMenu {
|
||||
public readonly sideToolbar: Locator
|
||||
public readonly themeToggleButton: Locator
|
||||
@@ -72,6 +75,28 @@ type FolderStructure = {
|
||||
[key: string]: FolderStructure | string
|
||||
}
|
||||
|
||||
type KeysOfType<T, Match> = {
|
||||
[K in keyof T]: T[K] extends Match ? K : never
|
||||
}[keyof T]
|
||||
|
||||
class ConfirmDialog {
|
||||
public readonly delete: Locator
|
||||
public readonly overwrite: Locator
|
||||
public readonly reject: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.delete = page.locator('button.p-button[aria-label="Delete"]')
|
||||
this.overwrite = page.locator('button.p-button[aria-label="Overwrite"]')
|
||||
this.reject = page.locator('button.p-button[aria-label="Cancel"]')
|
||||
}
|
||||
|
||||
async click(locator: KeysOfType<ConfirmDialog, Locator>) {
|
||||
const loc = this[locator]
|
||||
await expect(loc).toBeVisible()
|
||||
await loc.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyPage {
|
||||
public readonly url: string
|
||||
// All canvas position operations are based on default view of canvas.
|
||||
@@ -91,6 +116,7 @@ export class ComfyPage {
|
||||
public readonly actionbar: ComfyActionbar
|
||||
public readonly templates: ComfyTemplates
|
||||
public readonly settingDialog: SettingDialog
|
||||
public readonly confirmDialog: ConfirmDialog
|
||||
|
||||
/** Worker index to test user ID */
|
||||
public readonly userIds: string[] = []
|
||||
@@ -115,6 +141,7 @@ export class ComfyPage {
|
||||
this.actionbar = new ComfyActionbar(page)
|
||||
this.templates = new ComfyTemplates(page)
|
||||
this.settingDialog = new SettingDialog(page)
|
||||
this.confirmDialog = new ConfirmDialog(page)
|
||||
}
|
||||
|
||||
convertLeafToContent(structure: FolderStructure): FolderStructure {
|
||||
@@ -487,6 +514,10 @@ export class ComfyPage {
|
||||
return { x: 427, y: 98 }
|
||||
}
|
||||
|
||||
get promptDialogInput() {
|
||||
return this.page.locator('.p-dialog-content input[type="text"]')
|
||||
}
|
||||
|
||||
async disconnectEdge() {
|
||||
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
|
||||
}
|
||||
@@ -737,28 +768,29 @@ export class ComfyPage {
|
||||
)
|
||||
}
|
||||
|
||||
async confirmDialog(prompt: string, text: string = 'Yes') {
|
||||
async clickDialogButton(prompt: string, buttonText: string = 'Yes') {
|
||||
const modal = this.page.locator(
|
||||
`.comfy-modal-content:has-text("${prompt}")`
|
||||
)
|
||||
await expect(modal).toBeVisible()
|
||||
await modal
|
||||
.locator('.comfyui-button', {
|
||||
hasText: text
|
||||
hasText: buttonText
|
||||
})
|
||||
.click()
|
||||
await expect(modal).toBeHidden()
|
||||
}
|
||||
|
||||
async convertAllNodesToGroupNode(groupNodeName: string) {
|
||||
this.page.on('dialog', async (dialog) => {
|
||||
await dialog.accept(groupNodeName)
|
||||
})
|
||||
await this.canvas.press('Control+a')
|
||||
const node = await this.getFirstNodeRef()
|
||||
await node!.clickContextMenuOption('Convert to Group Node')
|
||||
await this.promptDialogInput.fill(groupNodeName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
await this.promptDialogInput.waitFor({ state: 'hidden' })
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async convertOffsetToCanvas(pos: [number, number]) {
|
||||
return this.page.evaluate((pos) => {
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas(pos)
|
||||
@@ -788,6 +820,31 @@ export class ComfyPage {
|
||||
async moveMouseToEmptyArea() {
|
||||
await this.page.mouse.move(10, 10)
|
||||
}
|
||||
async getUndoQueueSize() {
|
||||
return this.page.evaluate(() => {
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.undoQueue.length
|
||||
})
|
||||
}
|
||||
async getRedoQueueSize() {
|
||||
return this.page.evaluate(() => {
|
||||
const workflow = (window['app'].extensionManager as WorkspaceStore)
|
||||
.workflow.activeWorkflow
|
||||
return workflow?.changeTracker.redoQueue.length
|
||||
})
|
||||
}
|
||||
async isCurrentWorkflowModified() {
|
||||
return this.page.evaluate(() => {
|
||||
return (window['app'].extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow?.isModified
|
||||
})
|
||||
}
|
||||
async getExportedWorkflow({ api = false }: { api?: boolean } = {}) {
|
||||
return this.page.evaluate(async (api) => {
|
||||
return (await window['app'].graphToPrompt())[api ? 'output' : 'workflow']
|
||||
}, api)
|
||||
}
|
||||
}
|
||||
|
||||
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
@@ -799,18 +856,23 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
const userId = await comfyPage.setupUser(username)
|
||||
comfyPage.userIds[parallelIndex] = userId
|
||||
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Disabled',
|
||||
// Hide canvas menu/info by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': false,
|
||||
// Hide all badges by default.
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
|
||||
// Disable tooltips by default to avoid flakiness.
|
||||
'Comfy.EnableTooltips': false,
|
||||
'Comfy.userId': userId
|
||||
})
|
||||
try {
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Disabled',
|
||||
// Hide canvas menu/info by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': false,
|
||||
// Hide all badges by default.
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
|
||||
// Disable tooltips by default to avoid flakiness.
|
||||
'Comfy.EnableTooltips': false,
|
||||
'Comfy.userId': userId
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
await comfyPage.setup()
|
||||
await use(comfyPage)
|
||||
}
|
||||
|
||||
41
browser_tests/fixtures/UserSelectPage.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Page } from 'playwright'
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
export class UserSelectPage {
|
||||
constructor(
|
||||
public readonly url: string,
|
||||
public readonly page: Page
|
||||
) {}
|
||||
|
||||
get selectionUrl() {
|
||||
return this.url + '/user-select'
|
||||
}
|
||||
|
||||
get container() {
|
||||
return this.page.locator('#comfy-user-selection')
|
||||
}
|
||||
|
||||
get newUserInput() {
|
||||
return this.container.locator('#new-user-input')
|
||||
}
|
||||
|
||||
get existingUserSelect() {
|
||||
return this.container.locator('#existing-user-select')
|
||||
}
|
||||
|
||||
get nextButton() {
|
||||
return this.container.getByText('Next')
|
||||
}
|
||||
}
|
||||
|
||||
export const userSelectPageFixture = base.extend<{
|
||||
userSelectPage: UserSelectPage
|
||||
}>({
|
||||
userSelectPage: async ({ page }, use) => {
|
||||
const userSelectPage = new UserSelectPage(
|
||||
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188',
|
||||
page
|
||||
)
|
||||
await use(userSelectPage)
|
||||
}
|
||||
})
|
||||
@@ -85,32 +85,36 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
super(page, 'workflows')
|
||||
}
|
||||
|
||||
get root() {
|
||||
return this.page.locator('.workflows-sidebar-tab')
|
||||
}
|
||||
|
||||
get browseGalleryButton() {
|
||||
return this.page.locator('.browse-templates-button')
|
||||
return this.root.locator('.browse-templates-button')
|
||||
}
|
||||
|
||||
get newBlankWorkflowButton() {
|
||||
return this.page.locator('.new-blank-workflow-button')
|
||||
return this.root.locator('.new-blank-workflow-button')
|
||||
}
|
||||
|
||||
get openWorkflowButton() {
|
||||
return this.page.locator('.open-workflow-button')
|
||||
return this.root.locator('.open-workflow-button')
|
||||
}
|
||||
|
||||
async getOpenedWorkflowNames() {
|
||||
return await this.page
|
||||
return await this.root
|
||||
.locator('.comfyui-workflows-open .node-label')
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async getActiveWorkflowName() {
|
||||
return await this.page
|
||||
return await this.root
|
||||
.locator('.comfyui-workflows-open .p-tree-node-selected .node-label')
|
||||
.innerText()
|
||||
}
|
||||
|
||||
async getTopLevelSavedWorkflowNames() {
|
||||
return await this.page
|
||||
return await this.root
|
||||
.locator('.comfyui-workflows-browse .node-label')
|
||||
.allInnerTexts()
|
||||
}
|
||||
@@ -122,13 +126,13 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
}
|
||||
|
||||
getOpenedItem(name: string) {
|
||||
return this.page.locator('.comfyui-workflows-open .node-label', {
|
||||
return this.root.locator('.comfyui-workflows-open .node-label', {
|
||||
hasText: name
|
||||
})
|
||||
}
|
||||
|
||||
getPersistedItem(name: string) {
|
||||
return this.page.locator('.comfyui-workflows-browse .node-label', {
|
||||
return this.root.locator('.comfyui-workflows-browse .node-label', {
|
||||
hasText: name
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,7 +40,14 @@ export class Topbar {
|
||||
return this._saveWorkflow(workflowName, 'Save As')
|
||||
}
|
||||
|
||||
async _saveWorkflow(workflowName: string, command: 'Save' | 'Save As') {
|
||||
exportWorkflow(workflowName: string): Promise<void> {
|
||||
return this._saveWorkflow(workflowName, 'Export')
|
||||
}
|
||||
|
||||
async _saveWorkflow(
|
||||
workflowName: string,
|
||||
command: 'Save' | 'Save As' | 'Export'
|
||||
) {
|
||||
await this.triggerTopbarCommand(['Workflow', command])
|
||||
await this.getSaveDialog().fill(workflowName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
|
||||
@@ -233,10 +233,10 @@ export class NodeReference {
|
||||
await ctx.getByText(optionText).click()
|
||||
}
|
||||
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
|
||||
this.comfyPage.page.once('dialog', async (dialog) => {
|
||||
await dialog.accept(groupNodeName)
|
||||
})
|
||||
await this.clickContextMenuOption('Convert to Group Node')
|
||||
await this.comfyPage.promptDialogInput.fill(groupNodeName)
|
||||
await this.comfyPage.page.keyboard.press('Enter')
|
||||
await this.comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
|
||||
await this.comfyPage.nextFrame()
|
||||
const nodes = await this.comfyPage.getNodeRefsByType(
|
||||
`workflow>${groupNodeName}`
|
||||
|
||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -103,6 +103,36 @@ test.describe('Group Node', () => {
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Manage group opens with the correct group selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const makeGroup = async (name, type1, type2) => {
|
||||
const node1 = (await comfyPage.getNodeRefsByType(type1))[0]
|
||||
const node2 = (await comfyPage.getNodeRefsByType(type2))[0]
|
||||
await node1.click('title')
|
||||
await node2.click('title', {
|
||||
modifiers: ['Shift']
|
||||
})
|
||||
return await node2.convertToGroupNode(name)
|
||||
}
|
||||
|
||||
const group1 = await makeGroup(
|
||||
'g1',
|
||||
'CLIPTextEncode',
|
||||
'CheckpointLoaderSimple'
|
||||
)
|
||||
const group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler')
|
||||
|
||||
const manage1 = await group1.manageGroupNode()
|
||||
await comfyPage.nextFrame()
|
||||
expect(await manage1.getSelectedNodeType()).toBe('g1')
|
||||
await manage1.close()
|
||||
await expect(manage1.root).not.toBeVisible()
|
||||
|
||||
const manage2 = await group2.manageGroupNode()
|
||||
expect(await manage2.getSelectedNodeType()).toBe('g2')
|
||||
})
|
||||
|
||||
test('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
export class ManageGroupNode {
|
||||
footer: Locator
|
||||
header: Locator
|
||||
|
||||
constructor(
|
||||
readonly page: Page,
|
||||
readonly root: Locator
|
||||
) {
|
||||
this.footer = root.locator('footer')
|
||||
this.header = root.locator('header')
|
||||
}
|
||||
|
||||
async setLabel(name: string, label: string) {
|
||||
@@ -23,6 +25,11 @@ export class ManageGroupNode {
|
||||
await this.footer.getByText('Close').click()
|
||||
}
|
||||
|
||||
async getSelectedNodeType() {
|
||||
const select = this.header.locator('select').first()
|
||||
return await select.inputValue()
|
||||
}
|
||||
|
||||
async selectNode(name: string) {
|
||||
const list = this.root.locator('.comfy-group-manage-list-items')
|
||||
const item = list.getByText(name)
|
||||
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -350,23 +350,6 @@ test.describe('Menu', () => {
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
|
||||
})
|
||||
|
||||
test('Can migrate legacy bookmarks', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks', [
|
||||
'foo/',
|
||||
'foo/KSampler (Advanced)',
|
||||
'UNKNOWN',
|
||||
'KSampler'
|
||||
])
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
|
||||
await comfyPage.reload()
|
||||
expect(await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks')).toEqual(
|
||||
[]
|
||||
)
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
|
||||
).toEqual(['foo/', 'foo/KSamplerAdvanced', 'KSampler'])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Workflows sidebar', () => {
|
||||
@@ -452,6 +435,56 @@ test.describe('Menu', () => {
|
||||
).toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
|
||||
})
|
||||
|
||||
test('Exported workflow does not contain localized slot names', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
const exportedWorkflow = await comfyPage.getExportedWorkflow({
|
||||
api: false
|
||||
})
|
||||
expect(exportedWorkflow).toBeDefined()
|
||||
for (const node of exportedWorkflow.nodes) {
|
||||
for (const slot of node.inputs) {
|
||||
expect(slot.localized_name).toBeUndefined()
|
||||
expect(slot.label).toBeUndefined()
|
||||
}
|
||||
for (const slot of node.outputs) {
|
||||
expect(slot.localized_name).toBeUndefined()
|
||||
expect(slot.label).toBeUndefined()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Can export same workflow with different locales', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
|
||||
// Setup download listener before triggering the export
|
||||
const downloadPromise = comfyPage.page.waitForEvent('download')
|
||||
await comfyPage.menu.topbar.exportWorkflow('exported_default.json')
|
||||
|
||||
// Wait for the download and get the file content
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toBe('exported_default.json')
|
||||
|
||||
// Get the exported workflow content
|
||||
const downloadedContent = await comfyPage.getExportedWorkflow({
|
||||
api: false
|
||||
})
|
||||
|
||||
await comfyPage.setSetting('Comfy.Locale', 'zh')
|
||||
await comfyPage.reload()
|
||||
|
||||
const downloadedContentZh = await comfyPage.getExportedWorkflow({
|
||||
api: false
|
||||
})
|
||||
|
||||
// Compare the exported workflow with the original
|
||||
expect(downloadedContent).toBeDefined()
|
||||
expect(downloadedContent).toEqual(downloadedContentZh)
|
||||
})
|
||||
|
||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||
expect(
|
||||
@@ -459,12 +492,26 @@ test.describe('Menu', () => {
|
||||
).toEqual(['workflow5.json'])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow5.json'])
|
||||
})
|
||||
|
||||
test('Can save temporary workflow with unmodified name', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflow('Unsaved Workflow')
|
||||
// Should not trigger the overwrite dialog
|
||||
expect(
|
||||
await comfyPage.page.locator('.comfy-modal-content:visible').count()
|
||||
).toBe(0)
|
||||
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
})
|
||||
|
||||
test('Can overwrite other workflows with save as', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -480,7 +527,7 @@ test.describe('Menu', () => {
|
||||
)
|
||||
|
||||
await topbar.saveWorkflowAs('workflow1.json')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
// The old workflow1.json should be deleted and the new one should be saved.
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
@@ -522,6 +569,43 @@ test.describe('Menu', () => {
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['*Unsaved Workflow.json'])
|
||||
})
|
||||
|
||||
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false)
|
||||
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.clickContextMenuItem('Delete')
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can delete workflows', async ({ comfyPage }) => {
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
|
||||
await comfyPage.clickContextMenuItem('Delete')
|
||||
|
||||
await comfyPage.confirmDialog.click('delete')
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Workflows topbar tabs', () => {
|
||||
|
||||
@@ -48,4 +48,8 @@ test.describe('Optional input', () => {
|
||||
expect(vaeInput.link).toBeNull()
|
||||
expect(convertedInput.link).not.toBeNull()
|
||||
})
|
||||
test('slider', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('simple_slider')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -32,11 +32,11 @@ test.describe('Canvas Right Click Menu', () => {
|
||||
test('Can convert to group node', async ({ comfyPage }) => {
|
||||
await comfyPage.select2Nodes()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
|
||||
comfyPage.page.on('dialog', async (dialog) => {
|
||||
await dialog.accept('GroupNode2CLIP')
|
||||
})
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node')
|
||||
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-group-node.png'
|
||||
@@ -186,4 +186,22 @@ test.describe('Node Right Click Menu', () => {
|
||||
'selected-nodes-unpinned.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can clone pinned nodes', async ({ comfyPage }) => {
|
||||
const nodeCount = await comfyPage.getGraphNodesCount()
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
await node.clickContextMenuOption('Pin')
|
||||
await comfyPage.nextFrame()
|
||||
await node.click('title', { button: 'right' })
|
||||
await expect(
|
||||
comfyPage.page.locator('.litemenu-entry:has-text("Unpin")')
|
||||
).toBeAttached()
|
||||
const cloneItem = comfyPage.page.locator(
|
||||
'.litemenu-entry:has-text("Clone")'
|
||||
)
|
||||
await cloneItem.click()
|
||||
await expect(cloneItem).toHaveCount(0)
|
||||
await comfyPage.nextFrame()
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount + 1)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
42
browser_tests/userSelectView.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { userSelectPageFixture as test } from './fixtures/UserSelectPage'
|
||||
|
||||
/**
|
||||
* Expects ComfyUI backend to be launched with `--multi-user` flag.
|
||||
*/
|
||||
test.describe('User Select View', () => {
|
||||
test.beforeEach(async ({ userSelectPage, page }) => {
|
||||
await page.goto(userSelectPage.url)
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
})
|
||||
|
||||
test('Redirects to user select view if no user is logged in', async ({
|
||||
userSelectPage,
|
||||
page
|
||||
}) => {
|
||||
await page.goto(userSelectPage.url)
|
||||
await expect(userSelectPage.container).toBeVisible()
|
||||
expect(page.url()).toBe(userSelectPage.selectionUrl)
|
||||
})
|
||||
|
||||
test('Can create new user', async ({ userSelectPage, page }) => {
|
||||
const randomUser = `test-user-${Math.random().toString(36).substring(2, 7)}`
|
||||
await page.goto(userSelectPage.url)
|
||||
await expect(page).toHaveURL(userSelectPage.selectionUrl)
|
||||
await userSelectPage.newUserInput.fill(randomUser)
|
||||
await userSelectPage.nextButton.click()
|
||||
await expect(page).toHaveURL(userSelectPage.url)
|
||||
})
|
||||
|
||||
test('Can choose existing user', async ({ userSelectPage, page }) => {
|
||||
await page.goto(userSelectPage.url)
|
||||
await expect(page).toHaveURL(userSelectPage.selectionUrl)
|
||||
await userSelectPage.existingUserSelect.click()
|
||||
await page.locator('.p-select-list .p-select-option').first().click()
|
||||
await userSelectPage.nextButton.click()
|
||||
await expect(page).toHaveURL(userSelectPage.url)
|
||||
})
|
||||
})
|
||||
27
index.html
@@ -9,33 +9,6 @@
|
||||
</head>
|
||||
<body class="litegraph grid">
|
||||
<div id="vue-app"></div>
|
||||
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
|
||||
<main class="comfy-user-selection-inner">
|
||||
<h1>ComfyUI</h1>
|
||||
<form>
|
||||
<section>
|
||||
<label>New user:
|
||||
<input placeholder="Enter a username" />
|
||||
</label>
|
||||
</section>
|
||||
<div class="comfy-user-existing">
|
||||
<span class="or-separator">OR</span>
|
||||
<section>
|
||||
<label>
|
||||
Existing user:
|
||||
<select>
|
||||
<option hidden disabled selected value> Select a user </option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
<span class="comfy-user-error"> </span>
|
||||
<button class="comfy-btn comfy-user-button-next">Next</button>
|
||||
</footer>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,6 +3,7 @@ export default {
|
||||
|
||||
'./**/*.{ts,tsx,vue}': (stagedFiles) => [
|
||||
...formatFiles(stagedFiles),
|
||||
'vue-tsc --noEmit',
|
||||
'tsc --noEmit',
|
||||
'tsc-strict'
|
||||
]
|
||||
|
||||
5012
package-lock.json
generated
22
package.json
@@ -1,17 +1,22 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.4.9",
|
||||
"version": "1.6.3",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:electron": "vite --config vite.electron.config.mts",
|
||||
"build": "npm run typecheck && vite build",
|
||||
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"deploy": "npm run build && node scripts/deploy.js",
|
||||
"release": "node scripts/release.js",
|
||||
"update-litegraph": "node scripts/update-litegraph.js",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"typecheck": "tsc --noEmit && tsc-strict",
|
||||
"typecheck": "vue-tsc --noEmit && tsc --noEmit && tsc-strict",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue}'",
|
||||
"test": "jest --config jest.config.base.ts",
|
||||
"test:jest:fast": "jest --config jest.config.fast.ts",
|
||||
@@ -23,13 +28,16 @@
|
||||
"prepare": "husky || true",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix"
|
||||
"lint:fix": "eslint src --fix",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.7",
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@iconify/json": "^2.2.245",
|
||||
"@lobehub/i18n-cli": "^1.20.0",
|
||||
"@pinia/testing": "^0.1.5",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
@@ -66,14 +74,16 @@
|
||||
"unplugin-icons": "^0.19.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"vitest": "^2.0.5",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.6",
|
||||
"@comfyorg/litegraph": "^0.8.35",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.34",
|
||||
"@comfyorg/litegraph": "^0.8.47",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
|
||||
14
playwright.i18n.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { PlaywrightTestConfig } from '@playwright/test'
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './scripts',
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
headless: true
|
||||
},
|
||||
reporter: 'list',
|
||||
timeout: 10000,
|
||||
testMatch: /collect-i18n\.ts/
|
||||
}
|
||||
|
||||
export default config
|
||||
1
public/assets/images/Git-Logo-White.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="292" height="92pt" viewBox="0 0 219 92"><defs><clipPath id="a"><path d="M159 .79h25V69h-25Zm0 0"/></clipPath><clipPath id="b"><path d="M183 9h35.371v60H183Zm0 0"/></clipPath><clipPath id="c"><path d="M0 .79h92V92H0Zm0 0"/></clipPath></defs><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61"/><g clip-path="url(#a)"><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805"/></g><g clip-path="url(#b)"><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66"/></g><g clip-path="url(#c)"><path style="stroke:none;fill-rule:nonzero;fill:#fff;fill-opacity:1" d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/assets/images/apple-mps-logo.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
5
public/assets/images/manual-configuration.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="21.59mm" height="6.922mm" version="1.1" viewBox="0 0 21.59 6.922" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m6.667 0.941v1.345h-0.305v-1.345h-0.699v1.345h-0.304v-1.651h0.304v0.291q3e-3 -0.06 0.027-0.113 0.024-0.054 0.065-0.093 0.041-0.04 0.096-0.062 0.054-0.023 0.116-0.023h0.393q0.06 0 0.114 0.023 0.054 0.021 0.096 0.062 0.041 0.038 0.066 0.093 0.026 0.052 0.027 0.113 3e-3 -0.06 0.026-0.113 0.024-0.054 0.065-0.093 0.041-0.04 0.096-0.062 0.054-0.023 0.116-0.023h0.393q0.063 0 0.119 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119v1.347h-0.298v-1.345zm1.512 0.624q0-0.063 0.023-0.117 0.024-0.055 0.065-0.097 0.041-0.041 0.097-0.065 0.055-0.024 0.117-0.024h0.787v-0.321h-0.996v-0.305h0.996q0.063 0 0.119 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119v1.346h-0.302v-0.279q-4e-3 0.057-0.031 0.108-0.026 0.051-0.068 0.089-0.04 0.037-0.093 0.058-0.052 0.021-0.111 0.021h-0.483q-0.062 0-0.117-0.023-0.055-0.024-0.097-0.065-0.04-0.041-0.065-0.097-0.023-0.055-0.023-0.119zm0.303 0.415h0.787v-0.415h-0.786zm3.063 0.306h-0.306v-1.345h-0.851v1.345h-0.304v-1.651h0.303v0.291q3e-3 -0.06 0.027-0.113 0.024-0.054 0.065-0.093 0.041-0.04 0.096-0.062 0.054-0.023 0.116-0.023h0.545q0.063 0 0.119 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119zm0.508-1.651h0.303v1.346h0.851v-1.346h0.305v1.651h-0.304v-0.279q-4e-3 0.057-0.031 0.108-0.026 0.051-0.068 0.089-0.04 0.037-0.093 0.058-0.052 0.021-0.111 0.021h-0.547q-0.062 0-0.117-0.023-0.055-0.024-0.097-0.065-0.04-0.041-0.065-0.097-0.023-0.055-0.023-0.119zm1.969 0.93q0-0.063 0.023-0.117 0.024-0.055 0.065-0.097 0.041-0.041 0.097-0.065 0.055-0.024 0.117-0.024h0.787v-0.321h-0.996v-0.305h0.996q0.063 0 0.119 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119v1.346h-0.302v-0.279q-4e-3 0.057-0.031 0.108-0.026 0.051-0.068 0.089-0.04 0.037-0.093 0.058-0.052 0.021-0.111 0.021h-0.483q-0.062 0-0.117-0.023-0.055-0.024-0.097-0.065-0.04-0.041-0.065-0.097-0.023-0.055-0.023-0.119zm0.303 0.415h0.787v-0.415h-0.786zm1.906-1.98v2.286h-0.304v-2.286z" fill="#fff"/>
|
||||
<path d="m0.303 4.909v1.04h0.787v-0.279h0.305v0.279q0 0.063-0.024 0.119-0.023 0.055-0.065 0.097-0.04 0.04-0.096 0.065-0.055 0.023-0.119 0.023h-0.788q-0.062 0-0.117-0.023-0.056-0.023-0.098-0.063-0.04-0.042-0.065-0.098-0.023-0.056-0.023-0.119v-1.04q0-0.063 0.023-0.119 0.024-0.055 0.065-0.096t0.097-0.065q0.055-0.024 0.117-0.024h0.787q0.063 0 0.119 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119v0.279h-0.302v-0.279zm3.029 1.04q0 0.063-0.024 0.119-0.023 0.055-0.065 0.097-0.04 0.04-0.096 0.065-0.054 0.023-0.117 0.023h-0.821q-0.062 0-0.117-0.023-0.055-0.024-0.097-0.065-0.04-0.041-0.065-0.097-0.023-0.055-0.023-0.119v-1.04q0-0.063 0.023-0.119 0.024-0.055 0.065-0.096t0.097-0.065q0.055-0.024 0.117-0.024h0.82q0.063 0 0.117 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119zm-1.123-1.04v1.04h0.82v-1.04zm3.092 1.345h-0.305v-1.345h-0.851v1.345h-0.304v-1.651h0.303v0.291q3e-3 -0.06 0.027-0.113 0.024-0.054 0.065-0.093 0.041-0.04 0.096-0.062 0.054-0.023 0.116-0.023h0.545q0.063 0 0.119 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119zm1.12-1.981v0.33h0.542v0.305h-0.541v1.345h-0.305v-1.344h-0.403v-0.305h0.403v-0.33q0-0.063 0.023-0.117 0.024-0.055 0.066-0.097 0.041-0.041 0.097-0.065 0.055-0.024 0.117-0.024h0.542v0.305zm1.277 0.33v1.651h-0.305v-1.651zm-0.32-0.635h0.336v0.317h-0.336zm0.844 0.941q0-0.063 0.023-0.119 0.024-0.055 0.065-0.096t0.097-0.065q0.055-0.024 0.117-0.024h0.547q0.06 0 0.114 0.023 0.054 0.021 0.094 0.062 0.041 0.038 0.066 0.093 0.026 0.052 0.027 0.113v-0.291h0.305v2.012q0 0.063-0.024 0.119-0.023 0.055-0.065 0.096-0.04 0.041-0.096 0.065-0.055 0.024-0.119 0.024h-0.964v-0.305h0.964v-0.424h-0.851q-0.062 0-0.117-0.023-0.055-0.024-0.097-0.065-0.04-0.041-0.065-0.097-0.023-0.055-0.023-0.119zm1.154 0.976v-0.976h-0.851v0.976zm0.813-1.282h0.303v1.345h0.851v-1.345h0.305v1.651h-0.305v-0.279q-4e-3 0.057-0.031 0.108-0.026 0.051-0.068 0.089-0.04 0.037-0.093 0.058-0.052 0.021-0.111 0.021h-0.547q-0.062 0-0.117-0.023-0.055-0.024-0.097-0.065-0.04-0.041-0.065-0.097-0.023-0.055-0.023-0.119zm2.272 0.305v1.345h-0.303v-1.651h0.303v0.291q3e-3 -0.06 0.027-0.113 0.024-0.054 0.065-0.093 0.041-0.04 0.096-0.062 0.054-0.023 0.116-0.023h0.324q0.063 0 0.117 0.024 0.055 0.023 0.097 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119v0.279h-0.305v-0.279zm1.314 0.624q0-0.063 0.023-0.117 0.024-0.055 0.065-0.097 0.041-0.041 0.097-0.065 0.055-0.024 0.117-0.024h0.787v-0.319h-0.996v-0.305h0.996q0.063 0 0.119 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119v1.345h-0.305v-0.279q-4e-3 0.057-0.031 0.108-0.026 0.051-0.068 0.089-0.04 0.037-0.093 0.058-0.052 0.021-0.111 0.021h-0.483q-0.062 0-0.117-0.023-0.055-0.024-0.097-0.065-0.04-0.041-0.065-0.097-0.023-0.055-0.023-0.119zm0.303 0.415h0.787v-0.415h-0.787zm1.505-1.345h0.403v-0.508h0.305v0.508h0.542v0.305h-0.542v1.04h0.542v0.305h-0.542q-0.062 0-0.117-0.023-0.055-0.024-0.097-0.065-0.041-0.041-0.066-0.097-0.023-0.055-0.023-0.119v-1.04h-0.403zm2.08 0v1.651h-0.299v-1.649zm-0.314-0.634h0.336v0.317h-0.336zm2.272 1.981q0 0.063-0.024 0.119-0.023 0.055-0.065 0.097-0.04 0.04-0.096 0.065-0.054 0.023-0.117 0.023h-0.82q-0.062 0-0.117-0.023-0.055-0.024-0.097-0.065-0.04-0.041-0.065-0.097-0.023-0.055-0.023-0.119v-1.04q0-0.063 0.023-0.119 0.024-0.055 0.065-0.096t0.097-0.065q0.055-0.024 0.117-0.024h0.82q0.063 0 0.117 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119zm-1.123-1.04v1.04h0.82v-1.04zm3.092 1.345h-0.305v-1.345h-0.851v1.345h-0.303v-1.651h0.303v0.291q3e-3 -0.06 0.027-0.113 0.024-0.054 0.065-0.093 0.041-0.04 0.096-0.062 0.054-0.023 0.116-0.023h0.545q0.063 0 0.119 0.024 0.055 0.023 0.096 0.065 0.041 0.04 0.065 0.096 0.024 0.055 0.024 0.119z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
6
public/assets/images/nvidia-logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg enable-background="new 0 0 974.7 179.7" version="1.1" viewBox="0 0 974.7 179.7" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" width="110" height="44"><title> Artificial Intelligence Computing Leadership from NVIDIA</title>
|
||||
<path fill="#FFFFFF" d="m962.1 144.1v-2.7h1.7c0.9 0 2.2 0.1 2.2 1.2s-0.7 1.5-1.8 1.5h-2.1m0 1.9h1.2l2.7 4.7h2.9l-3-4.9c1.5 0.1 2.7-1 2.8-2.5v-0.4c0-2.6-1.8-3.4-4.8-3.4h-4.3v11.2h2.5v-4.7m12.6-0.9c0-6.6-5.1-10.4-10.8-10.4s-10.8 3.8-10.8 10.4 5.1 10.4 10.8 10.4 10.8-3.8 10.8-10.4m-3.2 0c0.2 4.2-3.1 7.8-7.3 8h-0.3c-4.4 0.2-8.1-3.3-8.3-7.7s3.3-8.1 7.7-8.3 8.1 3.3 8.3 7.7c-0.1 0.1-0.1 0.2-0.1 0.3z"></path>
|
||||
<path fill="#FFFFFF" d="m578.2 34v118h33.3v-118h-33.3zm-262-0.2v118.1h33.6v-91.7l26.2 0.1c8.6 0 14.6 2.1 18.7 6.5 5.3 5.6 7.4 14.7 7.4 31.2v53.9h32.6v-65.2c0-46.6-29.7-52.9-58.7-52.9h-59.8zm315.7 0.2v118h54c28.8 0 38.2-4.8 48.3-15.5 7.2-7.5 11.8-24.1 11.8-42.2 0-16.6-3.9-31.4-10.8-40.6-12.2-16.5-30-19.7-56.6-19.7h-46.7zm33 25.6h14.3c20.8 0 34.2 9.3 34.2 33.5s-13.4 33.6-34.2 33.6h-14.3v-67.1zm-134.7-25.6l-27.8 93.5-26.6-93.5h-36l38 118h48l38.4-118h-34zm231.4 118h33.3v-118h-33.3v118zm93.4-118l-46.5 117.9h32.8l7.4-20.9h55l7 20.8h35.7l-46.9-117.8h-44.5zm21.6 21.5l20.2 55.2h-41l20.8-55.2z">
|
||||
</path>
|
||||
<path fill="#76B900" d="m101.3 53.6v-16.2c1.6-0.1 3.2-0.2 4.8-0.2 44.4-1.4 73.5 38.2 73.5 38.2s-31.4 43.6-65.1 43.6c-4.5 0-8.9-0.7-13.1-2.1v-49.2c17.3 2.1 20.8 9.7 31.1 27l23.1-19.4s-16.9-22.1-45.3-22.1c-3-0.1-6 0.1-9 0.4m0-53.6v24.2l4.8-0.3c61.7-2.1 102 50.6 102 50.6s-46.2 56.2-94.3 56.2c-4.2 0-8.3-0.4-12.4-1.1v15c3.4 0.4 6.9 0.7 10.3 0.7 44.8 0 77.2-22.9 108.6-49.9 5.2 4.2 26.5 14.3 30.9 18.7-29.8 25-99.3 45.1-138.7 45.1-3.8 0-7.4-0.2-11-0.6v21.1h170.2v-179.7h-170.4zm0 116.9v12.8c-41.4-7.4-52.9-50.5-52.9-50.5s19.9-22 52.9-25.6v14h-0.1c-17.3-2.1-30.9 14.1-30.9 14.1s7.7 27.3 31 35.2m-73.5-39.5s24.5-36.2 73.6-40v-13.2c-54.4 4.4-101.4 50.4-101.4 50.4s26.6 77 101.3 84v-14c-54.8-6.8-73.5-67.2-73.5-67.2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/assets/images/sad_girl.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
254
scripts/collect-i18n.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import * as fs from 'fs'
|
||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
|
||||
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
|
||||
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
|
||||
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
|
||||
import type { ComfyCommandImpl } from '../src/stores/commandStore'
|
||||
import type { FormItem, SettingParams } from '../src/types/settingTypes'
|
||||
import type { ComfyApi } from '../src/scripts/api'
|
||||
|
||||
const localePath = './src/locales/en/main.json'
|
||||
const nodeDefsPath = './src/locales/en/nodeDefs.json'
|
||||
const commandsPath = './src/locales/en/commands.json'
|
||||
const settingsPath = './src/locales/en/settings.json'
|
||||
|
||||
const extractMenuCommandLocaleStrings = (): Set<string> => {
|
||||
const labels = new Set<string>()
|
||||
for (const [category, _] of CORE_MENU_COMMANDS) {
|
||||
category.forEach((category) => labels.add(category))
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
test('collect-i18n', async ({ comfyPage }) => {
|
||||
const commands = (
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const workspace = window['app'].extensionManager
|
||||
const commands = workspace.command.commands as ComfyCommandImpl[]
|
||||
return commands.map((command) => ({
|
||||
id: command.id,
|
||||
label: command.label,
|
||||
menubarLabel: command.menubarLabel,
|
||||
tooltip: command.tooltip
|
||||
}))
|
||||
})
|
||||
).sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
const locale = JSON.parse(fs.readFileSync(localePath, 'utf-8'))
|
||||
|
||||
// Commands
|
||||
const menuLabels = extractMenuCommandLocaleStrings()
|
||||
const commandMenuLabels = new Set(
|
||||
commands.map((command) => command.menubarLabel ?? command.label ?? '')
|
||||
)
|
||||
const allLabels = new Set([...menuLabels, ...commandMenuLabels])
|
||||
allLabels.delete('')
|
||||
|
||||
const allLabelsLocale = Object.fromEntries(
|
||||
Array.from(allLabels).map((label) => [normalizeI18nKey(label), label])
|
||||
)
|
||||
|
||||
const allCommandsLocale = Object.fromEntries(
|
||||
commands.map((command) => [
|
||||
normalizeI18nKey(command.id),
|
||||
{
|
||||
label: command.label,
|
||||
tooltip: command.tooltip
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
// Settings
|
||||
const settings = await comfyPage.page.evaluate(() => {
|
||||
const workspace = window['app'].extensionManager
|
||||
const settings = workspace.setting.settings as Record<string, SettingParams>
|
||||
return Object.values(settings)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.map((setting) => ({
|
||||
id: setting.id,
|
||||
name: setting.name,
|
||||
tooltip: setting.tooltip,
|
||||
category: setting.category,
|
||||
options: setting.options
|
||||
}))
|
||||
})
|
||||
|
||||
const allSettingsLocale = Object.fromEntries(
|
||||
settings.map((setting) => [
|
||||
normalizeI18nKey(setting.id),
|
||||
{
|
||||
name: setting.name,
|
||||
tooltip: setting.tooltip,
|
||||
// Don't translate the locale options as each option is in its own language.
|
||||
// e.g. "English", "中文", "Русский", "日本語", "한국어"
|
||||
options:
|
||||
setting.options && setting.id !== 'Comfy.Locale'
|
||||
? Object.fromEntries(
|
||||
setting.options.map((option) => {
|
||||
const optionLabel =
|
||||
typeof option === 'string' ? option : option.text
|
||||
return [normalizeI18nKey(optionLabel), optionLabel]
|
||||
})
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
const allSettingCategoriesLocale = Object.fromEntries(
|
||||
settings
|
||||
.flatMap((setting) => {
|
||||
return (setting.category ?? setting.id.split('.')).slice(0, 2)
|
||||
})
|
||||
.map((category: string) => [
|
||||
normalizeI18nKey(category),
|
||||
formatCamelCase(category)
|
||||
])
|
||||
)
|
||||
|
||||
// Server Configs
|
||||
const allServerConfigsLocale = Object.fromEntries(
|
||||
SERVER_CONFIG_ITEMS.map((config) => [
|
||||
normalizeI18nKey(config.id),
|
||||
{
|
||||
name: (config as unknown as FormItem).name,
|
||||
tooltip: (config as unknown as FormItem).tooltip
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
const allServerConfigCategoriesLocale = Object.fromEntries(
|
||||
SERVER_CONFIG_ITEMS.flatMap((config) => {
|
||||
return config.category ?? ['General']
|
||||
}).map((category) => [
|
||||
normalizeI18nKey(category),
|
||||
formatCamelCase(category)
|
||||
])
|
||||
)
|
||||
|
||||
// Node Definitions
|
||||
const nodeDefs: ComfyNodeDefImpl[] = Object.values(
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
const api = window['app'].api as ComfyApi
|
||||
return await api.getNodeDefs()
|
||||
})
|
||||
).map((def) => new ComfyNodeDefImpl(def))
|
||||
|
||||
console.log(`Collected ${nodeDefs.length} node definitions`)
|
||||
|
||||
const allDataTypesLocale = Object.fromEntries(
|
||||
nodeDefs
|
||||
.flatMap((nodeDef) => {
|
||||
const inputDataTypes = Object.values(nodeDef.inputs.all).map(
|
||||
(inputSpec) => inputSpec.type
|
||||
)
|
||||
const outputDataTypes = nodeDef.outputs.all.map((output) => output.type)
|
||||
const allDataTypes = [...inputDataTypes, ...outputDataTypes].flatMap(
|
||||
(type: string) => type.split(',')
|
||||
)
|
||||
return allDataTypes.map((dataType) => [
|
||||
normalizeI18nKey(dataType),
|
||||
dataType
|
||||
])
|
||||
})
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
)
|
||||
|
||||
function extractInputs(nodeDef: ComfyNodeDefImpl) {
|
||||
const inputs = Object.fromEntries(
|
||||
nodeDef.inputs.all.flatMap((input) => {
|
||||
const name = input.name
|
||||
const tooltip = input.tooltip
|
||||
|
||||
if (name === undefined && tooltip === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
normalizeI18nKey(input.name),
|
||||
{
|
||||
name,
|
||||
tooltip
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
)
|
||||
return Object.keys(inputs).length > 0 ? inputs : undefined
|
||||
}
|
||||
|
||||
function extractOutputs(nodeDef: ComfyNodeDefImpl) {
|
||||
const outputs = Object.fromEntries(
|
||||
nodeDef.outputs.all.flatMap((output, i) => {
|
||||
// Ignore data types if they are already translated in allDataTypesLocale.
|
||||
const name = output.name in allDataTypesLocale ? undefined : output.name
|
||||
const tooltip = output.tooltip
|
||||
|
||||
if (name === undefined && tooltip === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
i.toString(),
|
||||
{
|
||||
name,
|
||||
tooltip
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
)
|
||||
return Object.keys(outputs).length > 0 ? outputs : undefined
|
||||
}
|
||||
|
||||
const allNodeDefsLocale = Object.fromEntries(
|
||||
nodeDefs
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((nodeDef) => [
|
||||
normalizeI18nKey(nodeDef.name),
|
||||
{
|
||||
display_name: nodeDef.display_name ?? nodeDef.name,
|
||||
description: nodeDef.description || undefined,
|
||||
inputs: extractInputs(nodeDef),
|
||||
outputs: extractOutputs(nodeDef)
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
const allNodeCategoriesLocale = Object.fromEntries(
|
||||
nodeDefs.flatMap((nodeDef) =>
|
||||
nodeDef.category
|
||||
.split('/')
|
||||
.map((category) => [normalizeI18nKey(category), category])
|
||||
)
|
||||
)
|
||||
|
||||
fs.writeFileSync(
|
||||
localePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
...locale,
|
||||
menuLabels: allLabelsLocale,
|
||||
// Do merge for settingsCategories as there are some manual translations
|
||||
// for special panels like "About" and "Keybinding".
|
||||
settingsCategories: {
|
||||
...(locale.settingsCategories ?? {}),
|
||||
...allSettingCategoriesLocale
|
||||
},
|
||||
serverConfigItems: allServerConfigsLocale,
|
||||
serverConfigCategories: allServerConfigCategoriesLocale,
|
||||
dataTypes: allDataTypesLocale,
|
||||
nodeCategories: allNodeCategoriesLocale
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
fs.writeFileSync(nodeDefsPath, JSON.stringify(allNodeDefsLocale, null, 2))
|
||||
fs.writeFileSync(commandsPath, JSON.stringify(allCommandsLocale, null, 2))
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(allSettingsLocale, null, 2))
|
||||
})
|
||||
124
scripts/diff-i18n.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
readdirSync,
|
||||
mkdirSync,
|
||||
existsSync,
|
||||
rmSync
|
||||
} from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
|
||||
// Ensure directories exist
|
||||
function ensureDir(dir: string) {
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Read JSON file
|
||||
function readJsonFile(path: string) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, 'utf-8'))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all JSON files recursively
|
||||
function getAllJsonFiles(dir: string): string[] {
|
||||
const files: string[] = []
|
||||
const items = readdirSync(dir, { withFileTypes: true })
|
||||
|
||||
for (const item of items) {
|
||||
const path = join(dir, item.name)
|
||||
if (item.isDirectory()) {
|
||||
files.push(...getAllJsonFiles(path))
|
||||
} else if (item.name.endsWith('.json')) {
|
||||
files.push(path)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// Find additions in new object compared to base
|
||||
function findAdditions(base: any, updated: any): Record<string, any> {
|
||||
const additions: Record<string, any> = {}
|
||||
|
||||
for (const key in updated) {
|
||||
if (!(key in base)) {
|
||||
additions[key] = updated[key]
|
||||
} else if (
|
||||
typeof updated[key] === 'object' &&
|
||||
!Array.isArray(updated[key]) &&
|
||||
typeof base[key] === 'object' &&
|
||||
!Array.isArray(base[key])
|
||||
) {
|
||||
const nestedAdditions = findAdditions(base[key], updated[key])
|
||||
if (Object.keys(nestedAdditions).length > 0) {
|
||||
additions[key] = nestedAdditions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return additions
|
||||
}
|
||||
|
||||
// Capture command
|
||||
function capture(srcLocaleDir: string, tempBaseDir: string) {
|
||||
ensureDir(tempBaseDir)
|
||||
const files = getAllJsonFiles(srcLocaleDir)
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = file.replace(srcLocaleDir, '')
|
||||
const targetPath = join(tempBaseDir, relativePath)
|
||||
ensureDir(dirname(targetPath))
|
||||
writeFileSync(targetPath, readFileSync(file))
|
||||
}
|
||||
console.log('Captured current locale files to temp/base/')
|
||||
}
|
||||
|
||||
// Diff command
|
||||
function diff(srcLocaleDir: string, tempBaseDir: string, tempDiffDir: string) {
|
||||
ensureDir(tempDiffDir)
|
||||
const files = getAllJsonFiles(srcLocaleDir)
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = file.replace(srcLocaleDir, '')
|
||||
const basePath = join(tempBaseDir, relativePath)
|
||||
const diffPath = join(tempDiffDir, relativePath)
|
||||
|
||||
const baseContent = readJsonFile(basePath)
|
||||
const updatedContent = readJsonFile(file)
|
||||
|
||||
const additions = findAdditions(baseContent, updatedContent)
|
||||
if (Object.keys(additions).length > 0) {
|
||||
ensureDir(dirname(diffPath))
|
||||
writeFileSync(diffPath, JSON.stringify(additions, null, 2))
|
||||
console.log(`Wrote diff to ${diffPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Command handling
|
||||
const command = process.argv[2]
|
||||
const SRC_LOCALE_DIR = 'src/locales'
|
||||
const TEMP_BASE_DIR = 'temp/base'
|
||||
const TEMP_DIFF_DIR = 'temp/diff'
|
||||
|
||||
switch (command) {
|
||||
case 'capture':
|
||||
capture(SRC_LOCALE_DIR, TEMP_BASE_DIR)
|
||||
break
|
||||
case 'diff':
|
||||
diff(SRC_LOCALE_DIR, TEMP_BASE_DIR, TEMP_DIFF_DIR)
|
||||
break
|
||||
case 'clean':
|
||||
// Remove temp directory recursively
|
||||
if (existsSync('temp')) {
|
||||
rmSync('temp', { recursive: true, force: true })
|
||||
console.log('Removed temp directory')
|
||||
}
|
||||
break
|
||||
default:
|
||||
console.log('Please specify either "capture" or "diff" command')
|
||||
}
|
||||