Compare commits
47 Commits
v1.17.8
...
upstream-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bde964aed | ||
|
|
04f447c2a3 | ||
|
|
84c14ddd92 | ||
|
|
c7e6d66d47 | ||
|
|
5d4be8dc63 | ||
|
|
0bec26ca4b | ||
|
|
45eb4701d2 | ||
|
|
25359575db | ||
|
|
d0b99b95c6 | ||
|
|
ddff592561 | ||
|
|
8491ca91b7 | ||
|
|
630fa04882 | ||
|
|
bf80ae7295 | ||
|
|
44348180f5 | ||
|
|
4e12800336 | ||
|
|
d883448b86 | ||
|
|
1c59e3b51b | ||
|
|
b79cbf69af | ||
|
|
b05407ffdd | ||
|
|
2a62f7ec7f | ||
|
|
bf757c11ef | ||
|
|
0ed29a198d | ||
|
|
31d5671f24 | ||
|
|
ba3e2edb8a | ||
|
|
9c2300d780 | ||
|
|
3f85ff751c | ||
|
|
0caf1686c3 | ||
|
|
26f98d24fb | ||
|
|
fcbdee54ec | ||
|
|
a944372f39 | ||
|
|
df51e89311 | ||
|
|
b314435f81 | ||
|
|
ba367c0214 | ||
|
|
64ad6a9bb0 | ||
|
|
3819db5ec4 | ||
|
|
80517e8204 | ||
|
|
40034e77f9 | ||
|
|
2ef8b7cfd7 | ||
|
|
9cf3a0e568 | ||
|
|
f562cf27cd | ||
|
|
4244a0a258 | ||
|
|
ad3d2fe2e9 | ||
|
|
4c23cfbd4d | ||
|
|
9e10e55633 | ||
|
|
59cbe90fd3 | ||
|
|
16bd9abccd | ||
|
|
e84bdc96cf |
2
.github/workflows/update-electron-types.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
title: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
body: |
|
||||
|
||||
3
.gitignore
vendored
@@ -55,3 +55,6 @@ dist.zip
|
||||
|
||||
# Temporary repository directory
|
||||
templates_repo/
|
||||
|
||||
# Vite’s timestamped config modules
|
||||
vite.config.mts.timestamp-*.mjs
|
||||
|
||||
449
README.md
@@ -67,449 +67,6 @@ The development of successive minor versions overlaps. For example, while versio
|
||||
| 3 | Mar 15-21 | Released | Feature Freeze | Development | 1.1.7 through 1.1.13 (daily)<br>1.2.0 through 1.2.6 (daily) |
|
||||
| 4 | Mar 22-28 | - | Released | Feature Freeze | 1.2.7 through 1.2.13 (daily)<br>1.3.0 through 1.3.6 (daily) |
|
||||
|
||||
## Release Summary
|
||||
|
||||
### Major features
|
||||
|
||||
<details id='feature-native-translation'>
|
||||
<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 id='feature-mask-editor'>
|
||||
<summary>v1.4: New mask editor</summary>
|
||||
|
||||
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details id='feature-integrated-server-terminal'>
|
||||
<summary>v1.3.22: Integrated server terminal</summary>
|
||||
|
||||
Press Ctrl + ` to toggle integrated terminal.
|
||||
|
||||
https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
|
||||
</details>
|
||||
|
||||
<details id='feature-keybinding-customization'>
|
||||
<summary>v1.3.7: Keybinding customization</summary>
|
||||
|
||||
## Basic UI
|
||||

|
||||
|
||||
## Reset button
|
||||

|
||||
|
||||
## Edit Keybinding
|
||||

|
||||

|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/a3984ed9-eb28-4d47-86c0-7fc3efc2b5d0)
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-node-library-sidebar'>
|
||||
<summary>v1.2.4: Node library sidebar tab</summary>
|
||||
|
||||
#### Drag & Drop
|
||||
https://github.com/user-attachments/assets/853e20b7-bc0e-49c9-bbce-a2ba7566f92f
|
||||
|
||||
#### Filter
|
||||
https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
</details>
|
||||
|
||||
<details id='feature-queue-sidebar'>
|
||||
<summary>v1.2.0: Queue/History sidebar tab</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f
|
||||
</details>
|
||||
|
||||
<details id='feature-node-search'>
|
||||
<summary>v1.1.0: Node search box</summary>
|
||||
|
||||
#### Fuzzy search & Node preview
|
||||

|
||||
|
||||
#### Release link with shift
|
||||
https://github.com/user-attachments/assets/a1b2b5c3-10d1-4256-b620-345de6858f25
|
||||
</details>
|
||||
|
||||
### QoL changes
|
||||
|
||||
<details id='feature-nested-group'>
|
||||
<summary>v1.3.32: **Litegraph** Nested group</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
|
||||
</details>
|
||||
|
||||
<details id='feature-group-selection'>
|
||||
<summary>v1.3.24: **Litegraph** Group selection</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
|
||||
</details>
|
||||
|
||||
<details id='feature-toggle-link-visibility'>
|
||||
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-auto-widget-conversion'>
|
||||
<summary>v1.3.4: **Litegraph** Auto widget to input conversion</summary>
|
||||
|
||||
Dropping a link of correct type on node widget will automatically convert the widget to input.
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/15cea0b0-b225-4bec-af50-2cdb16dc46bf)
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-pan-mode'>
|
||||
<summary>v1.3.4: **Litegraph** Canvas pan mode</summary>
|
||||
|
||||
The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu
|
||||
or by holding the space key.
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/c7872532-a2ac-44c1-9e7d-9e03b5d1a80b)
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-shift-drag-link-creation'>
|
||||
<summary>v1.3.1: **Litegraph** Shift drag link to create a new link</summary>
|
||||
|
||||
[rec.webm](https://github.com/user-attachments/assets/7e73aaf9-79e2-4c3c-a26a-911cba3b85e4)
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-optional-input-donuts'>
|
||||
<summary>v1.2.62: **Litegraph** Show optional input slots as donuts</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-group-title-edit'>
|
||||
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/5bf0e2b6-8b3a-40a7-b44f-f0879e9ad26f
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-group-selection-shortcut'>
|
||||
<summary>v1.2.39: **Litegraph** Group selected nodes with Ctrl + G</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/7805dc54-0854-4a28-8bcd-4b007fa01151
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-node-title-edit'>
|
||||
<summary>v1.2.38: **Litegraph** Double click node title to edit</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/d61d5d0e-f200-4153-b293-3e3f6a212b30
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-drag-multi-link'>
|
||||
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/68826715-bb55-4b2a-be6e-675cfc424afe
|
||||
|
||||
https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
|
||||
</details>
|
||||
|
||||
<details id='feature-auto-connect-link'>
|
||||
<summary>v1.2.2: **Litegraph** auto connects to correct slot</summary>
|
||||
|
||||
#### Before
|
||||
https://github.com/user-attachments/assets/c253f778-82d5-4e6f-aec0-ea2ccf421651
|
||||
|
||||
#### After
|
||||
https://github.com/user-attachments/assets/b6360ac0-f0d2-447c-9daa-8a2e20c0dc1d
|
||||
</details>
|
||||
|
||||
<details id='feature-hide-text-overflow'>
|
||||
<summary>v1.1.8: **Litegraph** hides text overflow on widget value</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
|
||||
</details>
|
||||
|
||||
### Developer APIs
|
||||
|
||||
<details>
|
||||
<summary>v1.6.13: prompt/confirm/alert replacements for ComfyUI desktop</summary>
|
||||
|
||||
Several browser-only APIs are not available in ComfyUI desktop's electron environment.
|
||||
|
||||
- `window.prompt`
|
||||
- `window.confirm`
|
||||
- `window.alert`
|
||||
|
||||
Please use the following APIs as replacements.
|
||||
|
||||
```js
|
||||
// window.prompt
|
||||
window['app'].extensionManager.dialog
|
||||
.prompt({
|
||||
title: 'Test Prompt',
|
||||
message: 'Test Prompt Message'
|
||||
})
|
||||
.then((value: string) => {
|
||||
// Do something with the value user entered
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
|
||||
```js
|
||||
// window.confirm
|
||||
window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Test Confirm',
|
||||
message: 'Test Confirm Message'
|
||||
})
|
||||
.then((value: boolean) => {
|
||||
// Do something with the value user entered
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
|
||||
```js
|
||||
// window.alert
|
||||
window['app'].extensionManager.toast
|
||||
.addAlert("Test Alert")
|
||||
```
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.3.34: Register about panel badges</summary>
|
||||
|
||||
```js
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
aboutPageBadges: [
|
||||
{
|
||||
label: 'Test Badge',
|
||||
url: 'https://example.com',
|
||||
icon: 'pi pi-box'
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details id='extension-api-bottom-panel-tabs'>
|
||||
<summary>v1.3.22: Register bottom panel tabs</summary>
|
||||
|
||||
```js
|
||||
app.registerExtension({
|
||||
name: 'TestExtension',
|
||||
bottomPanelTabs: [
|
||||
{
|
||||
id: 'TestTab',
|
||||
title: 'Test Tab',
|
||||
type: 'custom',
|
||||
render: (el) => {
|
||||
el.innerHTML = '<div>Custom tab</div>'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details id='extension-api-settings'>
|
||||
<summary>v1.3.22: New settings API</summary>
|
||||
|
||||
Legacy settings API.
|
||||
|
||||
```js
|
||||
// Register a new setting
|
||||
app.ui.settings.addSetting({
|
||||
id: 'TestSetting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'Hello, world!'
|
||||
})
|
||||
|
||||
// Get the value of a setting
|
||||
const value = app.ui.settings.getSettingValue('TestSetting')
|
||||
|
||||
// Set the value of a setting
|
||||
app.ui.settings.setSettingValue('TestSetting', 'Hello, universe!')
|
||||
```
|
||||
|
||||
New settings API.
|
||||
|
||||
```js
|
||||
// Register a new setting
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
settings: [
|
||||
{
|
||||
id: 'TestSetting',
|
||||
name: 'Test Setting',
|
||||
type: 'text',
|
||||
defaultValue: 'Hello, world!'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Get the value of a setting
|
||||
const value = app.extensionManager.setting.get('TestSetting')
|
||||
|
||||
// Set the value of a setting
|
||||
app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details id='extension-api-commands-keybindings'>
|
||||
<summary>v1.3.7: Register commands and keybindings</summary>
|
||||
|
||||
Extensions can call the following API to register commands and keybindings. Do
|
||||
note that keybindings defined in core cannot be overwritten, and some keybindings
|
||||
are reserved by the browser.
|
||||
|
||||
```js
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
commands: [
|
||||
{
|
||||
id: 'TestCommand',
|
||||
function: () => {
|
||||
alert('TestCommand')
|
||||
}
|
||||
}
|
||||
],
|
||||
keybindings: [
|
||||
{
|
||||
combo: { key: 'k' },
|
||||
commandId: 'TestCommand'
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details id='extension-api-topbar-menu'>
|
||||
<summary>v1.3.1: Extension API to register custom topbar menu items</summary>
|
||||
|
||||
Extensions can call the following API to register custom topbar menu items.
|
||||
|
||||
```js
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
commands: [
|
||||
{
|
||||
id: 'foo-id',
|
||||
label: 'foo',
|
||||
function: () => {
|
||||
alert(1)
|
||||
}
|
||||
}
|
||||
],
|
||||
menuCommands: [
|
||||
{
|
||||
path: ['ext', 'ext2'],
|
||||
commands: ['foo-id']
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details id='extension-api-toast'>
|
||||
<summary>v1.2.27: Extension API to add toast message</summary>i
|
||||
|
||||
Extensions can call the following API to add toast messages.
|
||||
|
||||
```js
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Loaded!',
|
||||
detail: 'Extension loaded!',
|
||||
life: 3000
|
||||
})
|
||||
```
|
||||
Documentation of all supported options can be found here: <https://primevue.org/toast/#api.toast.interfaces.ToastMessageOptions>
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details id='extension-api-sidebar-tab'>
|
||||
<summary>v1.2.4: Extension API to register custom sidebar tab</summary>
|
||||
|
||||
Extensions now can call the following API to register a sidebar tab.
|
||||
|
||||
```js
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "search",
|
||||
icon: "pi pi-search",
|
||||
title: "search",
|
||||
tooltip: "search",
|
||||
type: "custom",
|
||||
render: (el) => {
|
||||
el.innerHTML = "<div>Custom search tab</div>";
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The list of supported icons can be found here: <https://primevue.org/icons/#list>
|
||||
|
||||
We will support custom icons later.
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details id='extension-api-selection-toolbox'>
|
||||
<summary>v1.10.9: Selection Toolbox API</summary>
|
||||
|
||||
Extensions can register commands that appear in the selection toolbox when specific items are selected on the canvas.
|
||||
|
||||
```js
|
||||
app.registerExtension({
|
||||
name: 'TestExtension1',
|
||||
commands: [
|
||||
{
|
||||
id: 'test.selection.command',
|
||||
label: 'Test Command',
|
||||
icon: 'pi pi-star',
|
||||
function: () => {
|
||||
// Command logic here
|
||||
}
|
||||
}
|
||||
],
|
||||
// Return an array of command IDs to show in the selection toolbox
|
||||
// when an item is selected
|
||||
getSelectionToolboxCommands: (selectedItem) => ['test.selection.command']
|
||||
})
|
||||
```
|
||||
|
||||
The selection toolbox will display the command button when items are selected:
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Development
|
||||
|
||||
### Tech Stack
|
||||
@@ -557,6 +114,12 @@ After you start the dev server, you should see following logs:
|
||||
Make sure your desktop machine and touch device are on the same network. On your touch device,
|
||||
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
|
||||
|
||||
### Recommended Code Editor Configuration
|
||||
|
||||
This project includes `.vscode/launch.json.default` and `.vscode/settings.json.default` files with recommended launch and workspace settings for editors that use the `.vscode` directory (e.g., VS Code, Cursor, etc.).
|
||||
|
||||
We’ve also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
|
||||
|
||||
### Unit Test
|
||||
|
||||
- `npm i` to install all dependencies
|
||||
|
||||
@@ -251,4 +251,17 @@ test.describe('Release context menu', () => {
|
||||
'link-release-context-menu.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can search and add node from context menu', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await comfyMouse.move({ x: 10, y: 10 })
|
||||
await comfyPage.clickContextMenuItem('Search')
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'link-context-menu-search.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
20
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.17.8",
|
||||
"version": "1.18.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.17.8",
|
||||
"version": "1.18.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.39",
|
||||
"@comfyorg/litegraph": "^0.13.8",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.42",
|
||||
"@comfyorg/litegraph": "^0.14.1",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -476,15 +476,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.4.39",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.39.tgz",
|
||||
"integrity": "sha512-5ZPaXy3SMMi5YO2gjgUWrRhmh/Ble1Qh//s+QKMyDP+FbMvVJu5eq6kc/5NiYngzaWziCcthgAlDBDz5oV5j4A==",
|
||||
"version": "0.4.42",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.42.tgz",
|
||||
"integrity": "sha512-sp3VIbpfRTcGdT3238aN7r4spRa2TtGewHlDrvT5GA2CdMOJ7Xn0Zig4rOja3MIogPhDVWGykIzbB5J08ViUSQ==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.13.8",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.13.8.tgz",
|
||||
"integrity": "sha512-NpnQpCM0rkAuiWSt7Yp2U9RmNqnK7L9xOye0xHu/avzdOMQTY6vy0g7VKopUrTSGMpeFbgYcnUh/W/XZcCx+sg==",
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.14.1.tgz",
|
||||
"integrity": "sha512-P/OlMsFHgkubvXrLhEJohQO+1e+xx6X/8yTXYFNj5Uz44GaXObaCDv+5guMSHASzuwiKMv4MPJbWSbdjeg5qVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.17.8",
|
||||
"version": "1.18.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -71,8 +71,8 @@
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.39",
|
||||
"@comfyorg/litegraph": "^0.13.8",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.42",
|
||||
"@comfyorg/litegraph": "^0.14.1",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<router-view />
|
||||
<ProgressSpinner
|
||||
v-if="isLoading"
|
||||
class="absolute inset-0 flex justify-center items-center h-screen"
|
||||
class="absolute inset-0 flex justify-center items-center h-[unset]"
|
||||
/>
|
||||
<GlobalDialog />
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
|
||||
115
src/components/BrowserTabTitle.spec.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, reactive } from 'vue'
|
||||
|
||||
import BrowserTabTitle from '@/components/BrowserTabTitle.vue'
|
||||
|
||||
// Mock the execution store
|
||||
const executionStore = reactive({
|
||||
isIdle: true,
|
||||
executionProgress: 0,
|
||||
executingNode: null as any,
|
||||
executingNodeProgress: 0
|
||||
})
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => executionStore
|
||||
}))
|
||||
|
||||
// Mock the setting store
|
||||
const settingStore = reactive({
|
||||
get: vi.fn(() => 'Enabled')
|
||||
})
|
||||
vi.mock('@/stores/settingStore', () => ({
|
||||
useSettingStore: () => settingStore
|
||||
}))
|
||||
|
||||
// Mock the workflow store
|
||||
const workflowStore = reactive({
|
||||
activeWorkflow: null as any
|
||||
})
|
||||
vi.mock('@/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => workflowStore
|
||||
}))
|
||||
|
||||
describe('BrowserTabTitle.vue', () => {
|
||||
let wrapper: ReturnType<typeof mount> | null
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = null
|
||||
// reset execution store
|
||||
executionStore.isIdle = true
|
||||
executionStore.executionProgress = 0
|
||||
executionStore.executingNode = null as any
|
||||
executionStore.executingNodeProgress = 0
|
||||
|
||||
// reset setting and workflow stores
|
||||
;(settingStore.get as any).mockReturnValue('Enabled')
|
||||
workflowStore.activeWorkflow = null
|
||||
|
||||
// reset document title
|
||||
document.title = ''
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
})
|
||||
|
||||
it('sets default title when idle and no workflow', () => {
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
expect(document.title).toBe('ComfyUI')
|
||||
})
|
||||
|
||||
it('sets workflow name as title when workflow exists and menu enabled', async () => {
|
||||
;(settingStore.get as any).mockReturnValue('Enabled')
|
||||
workflowStore.activeWorkflow = {
|
||||
filename: 'myFlow',
|
||||
isModified: false,
|
||||
isPersisted: true
|
||||
}
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('myFlow - ComfyUI')
|
||||
})
|
||||
|
||||
it('adds asterisk for unsaved workflow', async () => {
|
||||
;(settingStore.get as any).mockReturnValue('Enabled')
|
||||
workflowStore.activeWorkflow = {
|
||||
filename: 'myFlow',
|
||||
isModified: true,
|
||||
isPersisted: true
|
||||
}
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('*myFlow - ComfyUI')
|
||||
})
|
||||
|
||||
it('disables workflow title when menu disabled', async () => {
|
||||
;(settingStore.get as any).mockReturnValue('Disabled')
|
||||
workflowStore.activeWorkflow = {
|
||||
filename: 'myFlow',
|
||||
isModified: false,
|
||||
isPersisted: true
|
||||
}
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('ComfyUI')
|
||||
})
|
||||
|
||||
it('shows execution progress when not idle without workflow', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.3
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[30%]ComfyUI')
|
||||
})
|
||||
|
||||
it('shows node execution title when executing a node', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.executingNodeProgress = 0.5
|
||||
executionStore.executingNode = { type: 'Foo' }
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][50%] Foo')
|
||||
})
|
||||
})
|
||||
@@ -23,7 +23,7 @@ const executionText = computed(() =>
|
||||
)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const betaMenuEnabled = computed(
|
||||
const newMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ const nodeExecutionTitle = computed(() =>
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
executionText.value +
|
||||
(betaMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
(newMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
)
|
||||
|
||||
const title = computed(() => nodeExecutionTitle.value || workflowTitle.value)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<Splitter
|
||||
:key="activeSidebarTabId ?? undefined"
|
||||
:key="sidebarStateKey"
|
||||
class="splitter-overlay-root splitter-overlay"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
:state-key="activeSidebarTabId ?? undefined"
|
||||
:state-key="sidebarStateKey"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel
|
||||
@@ -59,6 +59,10 @@ const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
|
||||
const unifiedWidth = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.UnifiedWidth')
|
||||
)
|
||||
|
||||
const sidebarPanelVisible = computed(
|
||||
() => useSidebarTabStore().activeSidebarTab !== null
|
||||
)
|
||||
@@ -68,6 +72,10 @@ const bottomPanelVisible = computed(
|
||||
const activeSidebarTabId = computed(
|
||||
() => useSidebarTabStore().activeSidebarTabId
|
||||
)
|
||||
|
||||
const sidebarStateKey = computed(() => {
|
||||
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -5,7 +5,7 @@ import SelectButton from 'primevue/selectbutton'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import ColorCustomizationSelector from '../ColorCustomizationSelector.vue'
|
||||
import ColorCustomizationSelector from './ColorCustomizationSelector.vue'
|
||||
|
||||
describe('ColorCustomizationSelector', () => {
|
||||
const colorOptions = [
|
||||
@@ -4,7 +4,7 @@ import InputText from 'primevue/inputtext'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import EditableText from '../EditableText.vue'
|
||||
import EditableText from './EditableText.vue'
|
||||
|
||||
describe('EditableText', () => {
|
||||
beforeAll(() => {
|
||||
@@ -6,7 +6,7 @@ import InputText from 'primevue/inputtext'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import UrlInput from '../UrlInput.vue'
|
||||
import UrlInput from './UrlInput.vue'
|
||||
|
||||
describe('UrlInput', () => {
|
||||
beforeEach(() => {
|
||||
39
src/components/common/UserCredit.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div v-if="balanceLoading" class="flex items-center gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton shape="circle" width="1.5rem" height="1.5rem" />
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<Skeleton width="8rem" height="2rem" />
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<div :class="textClass">{{ formattedBalance }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
const { textClass } = defineProps<{
|
||||
textClass?: string
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
const formattedBalance = computed(() => {
|
||||
if (!authStore.balance) return '0.00'
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
})
|
||||
</script>
|
||||
@@ -12,7 +12,7 @@
|
||||
<ApiNodesList :node-names="apiNodeNames" />
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<Button :label="t('g.learnMore')" link />
|
||||
<Button :label="t('g.learnMore')" link @click="handleLearnMoreClick" />
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
:label="t('g.cancel')"
|
||||
@@ -37,4 +37,8 @@ const { apiNodeNames, onLogin, onCancel } = defineProps<{
|
||||
onLogin?: () => void
|
||||
onCancel?: () => void
|
||||
}>()
|
||||
|
||||
const handleLearnMoreClick = () => {
|
||||
window.open('https://www.comfy.org/faq', '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
import ManagerProgressDialogContent from '../ManagerProgressDialogContent.vue'
|
||||
import ManagerProgressDialogContent from './ManagerProgressDialogContent.vue'
|
||||
|
||||
type ComponentInstance = InstanceType<typeof ManagerProgressDialogContent> & {
|
||||
lastPanelRef: HTMLElement | null
|
||||
@@ -47,27 +47,10 @@
|
||||
<SettingsPanel :setting-groups="sortedGroups(category)" />
|
||||
</PanelTemplate>
|
||||
|
||||
<AboutPanel />
|
||||
<UserPanel />
|
||||
<CreditsPanel />
|
||||
<Suspense>
|
||||
<KeybindingPanel />
|
||||
<Suspense v-for="panel in panels" :key="panel.node.key">
|
||||
<component :is="panel.component" />
|
||||
<template #fallback>
|
||||
<div>Loading keybinding panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<ExtensionPanel />
|
||||
<template #fallback>
|
||||
<div>Loading extension panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<ServerConfigPanel />
|
||||
<template #fallback>
|
||||
<div>Loading server config panel...</div>
|
||||
<div>Loading {{ panel.node.label }} panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanels>
|
||||
@@ -81,24 +64,21 @@ import Listbox from 'primevue/listbox'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed, defineAsyncComponent, watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { useSettingSearch } from '@/composables/setting/useSettingSearch'
|
||||
import { useSettingUI } from '@/composables/setting/useSettingUI'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { SettingTreeNode } from '@/stores/settingStore'
|
||||
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
|
||||
import AboutPanel from './setting/AboutPanel.vue'
|
||||
import ColorPaletteMessage from './setting/ColorPaletteMessage.vue'
|
||||
import CreditsPanel from './setting/CreditsPanel.vue'
|
||||
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
|
||||
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
|
||||
import PanelTemplate from './setting/PanelTemplate.vue'
|
||||
import SettingsPanel from './setting/SettingsPanel.vue'
|
||||
import UserPanel from './setting/UserPanel.vue'
|
||||
|
||||
const { defaultPanel } = defineProps<{
|
||||
defaultPanel?:
|
||||
@@ -110,21 +90,12 @@ const { defaultPanel } = defineProps<{
|
||||
| 'credits'
|
||||
}>()
|
||||
|
||||
const KeybindingPanel = defineAsyncComponent(
|
||||
() => import('./setting/KeybindingPanel.vue')
|
||||
)
|
||||
const ExtensionPanel = defineAsyncComponent(
|
||||
() => import('./setting/ExtensionPanel.vue')
|
||||
)
|
||||
const ServerConfigPanel = defineAsyncComponent(
|
||||
() => import('./setting/ServerConfigPanel.vue')
|
||||
)
|
||||
|
||||
const {
|
||||
activeCategory,
|
||||
defaultCategory,
|
||||
settingCategories,
|
||||
groupedMenuTreeNodes
|
||||
groupedMenuTreeNodes,
|
||||
panels
|
||||
} = useSettingUI(defaultPanel)
|
||||
|
||||
const {
|
||||
@@ -136,7 +107,7 @@ const {
|
||||
getSearchResults
|
||||
} = useSettingSearch()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authService = useFirebaseAuthService()
|
||||
|
||||
// Sort groups for a category
|
||||
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
|
||||
@@ -169,7 +140,7 @@ watch(activeCategory, (_, oldValue) => {
|
||||
activeCategory.value = oldValue
|
||||
}
|
||||
if (activeCategory.value?.key === 'credits') {
|
||||
void authStore.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-4 p-4">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<i class="pi pi-exclamation-circle mb-4" style="font-size: 2rem" />
|
||||
<h2 class="text-2xl font-semibold mb-2">
|
||||
{{ $t(`auth.required.${type}.title`) }}
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-4 max-w-md">
|
||||
{{ $t(`auth.required.${type}.message`) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<Button
|
||||
class="w-60"
|
||||
severity="primary"
|
||||
:label="$t(`auth.required.${type}.action`)"
|
||||
@click="openPanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'signIn' | 'credits'
|
||||
}>()
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const openPanel = () => {
|
||||
// Close the current dialog
|
||||
dialogStore.closeDialog({ key: 'signin-required' })
|
||||
|
||||
// Open user settings and navigate to appropriate panel
|
||||
if (props.type === 'credits') {
|
||||
authStore.openCreditsPanel()
|
||||
} else {
|
||||
authStore.openSignInPanel()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,35 +1,59 @@
|
||||
<template>
|
||||
<div class="flex flex-col p-6">
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
:class="{ 'text-red-500': isInsufficientCredits }"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'text-2xl',
|
||||
isInsufficientCredits ? 'pi pi-exclamation-triangle' : ''
|
||||
]"
|
||||
/>
|
||||
<h2 class="text-2xl font-semibold">
|
||||
{{
|
||||
$t(
|
||||
isInsufficientCredits
|
||||
? 'credits.topUp.insufficientTitle'
|
||||
: 'credits.topUp.title'
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<div class="flex flex-col w-96 p-2 gap-10">
|
||||
<div v-if="isInsufficientCredits" class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ $t('credits.topUp.insufficientTitle') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
{{ $t('credits.topUp.insufficientMessage') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<p v-if="isInsufficientCredits" class="text-lg text-muted mt-6">
|
||||
{{ $t('credits.topUp.insufficientMessage') }}
|
||||
</p>
|
||||
|
||||
<!-- Balance Section -->
|
||||
<div class="flex justify-between items-center mt-8">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-muted">{{ $t('credits.yourCreditBalance') }}</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<div class="text-muted text-base">
|
||||
{{ $t('credits.yourCreditBalance') }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<UserCredit text-class="text-2xl" />
|
||||
<Button
|
||||
outlined
|
||||
severity="secondary"
|
||||
:label="$t('credits.topUp.seeDetails')"
|
||||
icon="pi pi-arrow-up-right"
|
||||
@click="handleSeeDetails"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount Input Section -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-muted text-sm"
|
||||
>{{ $t('credits.topUp.quickPurchase') }}:</span
|
||||
>
|
||||
<div class="grid grid-cols-[2fr_1fr] gap-2">
|
||||
<template v-for="amount in amountOptions" :key="amount">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-xl">{{ amount }}</span>
|
||||
</div>
|
||||
<Button
|
||||
:severity="
|
||||
preselectedAmountOption === amount ? 'primary' : 'secondary'
|
||||
"
|
||||
:outlined="preselectedAmountOption !== amount"
|
||||
:label="$t('credits.topUp.buyNow')"
|
||||
@click="handleBuyNow(amount)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
@@ -37,122 +61,79 @@
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-2xl">{{ formattedBalance }}</span>
|
||||
<InputNumber
|
||||
v-model="customAmount"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
show-buttons
|
||||
:allow-empty="false"
|
||||
:highlight-on-focus="true"
|
||||
pt:pc-input-text:root="w-24"
|
||||
@blur="
|
||||
(e: InputNumberBlurEvent) => (customAmount = Number(e.value))
|
||||
"
|
||||
@input="
|
||||
(e: InputNumberInputEvent) => (customAmount = Number(e.value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
text
|
||||
severity="secondary"
|
||||
:label="$t('credits.creditsHistory')"
|
||||
icon="pi pi-arrow-up-right"
|
||||
@click="handleSeeDetails"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Amount Input Section -->
|
||||
<div class="flex flex-col gap-2 mt-8">
|
||||
<div>
|
||||
<span class="text-muted">{{ $t('credits.topUp.addCredits') }}</span>
|
||||
<span class="text-muted text-sm ml-1">{{
|
||||
$t('credits.topUp.maxAmount')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||
<Button
|
||||
v-else
|
||||
:label="$t('credits.topUp.buyNow')"
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model="amount"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
show-buttons
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
outlined
|
||||
@click="handleBuyNow(customAmount)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mt-8">
|
||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||
<Button
|
||||
v-else
|
||||
severity="primary"
|
||||
:label="$t('credits.topUp.buyNow')"
|
||||
:disabled="!amount || amount > 1000"
|
||||
:pt="{
|
||||
root: { class: 'px-8' }
|
||||
}"
|
||||
@click="handleBuyNow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import InputNumber, {
|
||||
type InputNumberBlurEvent,
|
||||
type InputNumberInputEvent
|
||||
} from 'primevue/inputnumber'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency, usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
const {
|
||||
isInsufficientCredits = false,
|
||||
amountOptions = [5, 10, 20, 50],
|
||||
preselectedAmountOption = 10
|
||||
} = defineProps<{
|
||||
isInsufficientCredits?: boolean
|
||||
amountOptions?: number[]
|
||||
preselectedAmountOption?: number
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const amount = ref<number>(9.99)
|
||||
const authService = useFirebaseAuthService()
|
||||
const customAmount = ref<number>(100)
|
||||
const didClickBuyNow = ref(false)
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const handleBlur = (e: any) => {
|
||||
if (e.target.value) {
|
||||
amount.value = parseFloat(e.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = (e: any) => {
|
||||
amount.value = e.value
|
||||
}
|
||||
|
||||
const formattedBalance = computed(() => {
|
||||
if (!authStore.balance) return '0.000'
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
})
|
||||
|
||||
const handleSeeDetails = async () => {
|
||||
const response = await authStore.accessBillingPortal()
|
||||
if (!response?.billing_portal_url) return
|
||||
window.open(response.billing_portal_url, '_blank')
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleBuyNow = async () => {
|
||||
if (!amount.value) return
|
||||
|
||||
const response = await authStore.initiateCreditPurchase({
|
||||
amount_micros: usdToMicros(amount.value),
|
||||
currency: 'usd'
|
||||
})
|
||||
|
||||
if (!response?.checkout_url) return
|
||||
|
||||
const handleBuyNow = async (amount: number) => {
|
||||
await authService.purchaseCredits(amount)
|
||||
didClickBuyNow.value = true
|
||||
|
||||
// Go to Stripe checkout page
|
||||
window.open(response.checkout_url, '_blank')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (didClickBuyNow.value) {
|
||||
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
|
||||
void authStore.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMesages from '@/locales/en/main.json'
|
||||
import { IssueReportPanelProps } from '@/types/issueReportTypes'
|
||||
|
||||
import ReportIssuePanel from '../ReportIssuePanel.vue'
|
||||
import ReportIssuePanel from './ReportIssuePanel.vue'
|
||||
|
||||
const DEFAULT_FIELDS = ['Workflow', 'Logs', 'Settings', 'SystemStats']
|
||||
const CUSTOM_FIELDS = [
|
||||
@@ -142,6 +142,14 @@ vi.mock('@primevue/forms', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
currentUser: {
|
||||
email: 'test@example.com'
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
describe('ReportIssuePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -9,8 +9,8 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionBadge from '../PackVersionBadge.vue'
|
||||
import PackVersionSelectorPopover from '../PackVersionSelectorPopover.vue'
|
||||
import PackVersionBadge from './PackVersionBadge.vue'
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
@@ -10,7 +10,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionSelectorPopover from '../PackVersionSelectorPopover.vue'
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
// Default mock versions for reference
|
||||
const defaultMockVersions = [
|
||||
@@ -9,7 +9,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
import PackEnableToggle from '../PackEnableToggle.vue'
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
// Mock debounce to execute immediately
|
||||
vi.mock('lodash', () => ({
|
||||
@@ -5,7 +5,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
import DescriptionTabPanel from '../DescriptionTabPanel.vue'
|
||||
import DescriptionTabPanel from './DescriptionTabPanel.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -7,8 +7,8 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
import GridSkeleton from '../GridSkeleton.vue'
|
||||
import PackCardSkeleton from '../PackCardSkeleton.vue'
|
||||
import GridSkeleton from './GridSkeleton.vue'
|
||||
import PackCardSkeleton from './PackCardSkeleton.vue'
|
||||
|
||||
describe('GridSkeleton', () => {
|
||||
const mountComponent = ({
|
||||
@@ -12,22 +12,7 @@
|
||||
{{ $t('credits.yourCreditBalance') }}
|
||||
</h3>
|
||||
<div class="flex justify-between items-center">
|
||||
<div v-if="balanceLoading" class="flex items-center gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton shape="circle" width="1.5rem" height="1.5rem" />
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<Skeleton width="8rem" height="2rem" />
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<div class="text-3xl font-bold">{{ formattedBalance }}</div>
|
||||
</div>
|
||||
<UserCredit text-class="text-3xl font-bold" />
|
||||
<Skeleton v-if="loading" width="2rem" height="2rem" />
|
||||
<Button
|
||||
v-else
|
||||
@@ -51,14 +36,14 @@
|
||||
text
|
||||
size="small"
|
||||
severity="secondary"
|
||||
@click="() => authStore.fetchBalance()"
|
||||
@click="() => authService.fetchBalance()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-8">
|
||||
<Button
|
||||
:label="$t('credits.creditsHistory')"
|
||||
:label="$t('credits.invoiceHistory')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-up-right"
|
||||
@@ -123,11 +108,12 @@ import DataTable from 'primevue/datatable'
|
||||
import Divider from 'primevue/divider'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
@@ -141,12 +127,9 @@ interface CreditHistoryItemData {
|
||||
const { t } = useI18n()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authService = useFirebaseAuthService()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
const formattedBalance = computed(() => {
|
||||
if (!authStore.balance) return '0.00'
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
})
|
||||
|
||||
const formattedLastUpdateTime = computed(() =>
|
||||
authStore.lastBalanceUpdateTime
|
||||
@@ -159,13 +142,7 @@ const handlePurchaseCreditsClick = () => {
|
||||
}
|
||||
|
||||
const handleCreditsHistoryClick = async () => {
|
||||
const response = await authStore.accessBillingPortal()
|
||||
if (!response) return
|
||||
|
||||
const { billing_portal_url } = response
|
||||
if (billing_portal_url) {
|
||||
window.open(billing_portal_url, '_blank')
|
||||
}
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleMessageSupport = () => {
|
||||
@@ -180,7 +157,7 @@ const handleMessageSupport = () => {
|
||||
}
|
||||
|
||||
const handleFaqClick = () => {
|
||||
window.open('https://drip-art.notion.site/api-nodes-faqs', '_blank')
|
||||
window.open('https://www.comfy.org/faq', '_blank')
|
||||
}
|
||||
|
||||
const creditHistory = ref<CreditHistoryItemData[]>([])
|
||||
|
||||
@@ -4,7 +4,7 @@ import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SettingItem from '../SettingItem.vue'
|
||||
import SettingItem from './SettingItem.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -5,14 +5,13 @@
|
||||
<Divider class="mb-3" />
|
||||
|
||||
<div v-if="user" class="flex flex-col gap-2">
|
||||
<!-- User Avatar if available -->
|
||||
<div v-if="user.photoURL" class="flex items-center gap-2">
|
||||
<img
|
||||
:src="user.photoURL"
|
||||
:alt="user.displayName || ''"
|
||||
class="w-8 h-8 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<Avatar
|
||||
v-if="user.photoURL"
|
||||
:image="user.photoURL"
|
||||
shape="circle"
|
||||
size="large"
|
||||
aria-label="User Avatar"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h3 class="font-medium">
|
||||
@@ -77,21 +76,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Avatar from 'primevue/avatar'
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const toast = useToastStore()
|
||||
const { t } = useI18n()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const dialogService = useDialogService()
|
||||
const commandStore = useCommandStore()
|
||||
const user = computed(() => authStore.currentUser)
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
@@ -118,20 +114,10 @@ const providerIcon = computed(() => {
|
||||
})
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await authStore.logout()
|
||||
if (authStore.error) {
|
||||
toast.addAlert(authStore.error)
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.signOut.success'),
|
||||
detail: t('auth.signOut.successDetail'),
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
await commandStore.execute('Comfy.User.SignOut')
|
||||
}
|
||||
|
||||
const handleSignIn = async () => {
|
||||
await dialogService.showSignInDialog()
|
||||
await commandStore.execute('Comfy.User.OpenSignInDialog')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,7 +36,10 @@
|
||||
>
|
||||
{{ t('auth.login.passwordLabel') }}
|
||||
</label>
|
||||
<span class="text-muted text-base font-medium cursor-pointer">
|
||||
<span
|
||||
class="text-muted text-base font-medium cursor-pointer"
|
||||
@click="handleForgotPassword($form.email?.value)"
|
||||
>
|
||||
{{ t('auth.login.forgotPassword') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -78,9 +81,11 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const firebaseAuthService = useFirebaseAuthService()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -94,4 +99,9 @@ const onSubmit = (event: FormSubmitEvent) => {
|
||||
emit('submit', event.values as SignInData)
|
||||
}
|
||||
}
|
||||
|
||||
const handleForgotPassword = async (email: string) => {
|
||||
if (!email) return
|
||||
await firebaseAuthService.sendPasswordReset(email)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -35,8 +35,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CUDA_TORCH_URL,
|
||||
NIGHTLY_CPU_TORCH_URL,
|
||||
TorchDeviceType,
|
||||
TorchMirrorUrl
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
@@ -47,7 +45,6 @@ import { ModelRef, computed, onMounted, ref } from 'vue'
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
|
||||
|
||||
@@ -57,38 +54,20 @@ const pythonMirror = defineModel<string>('pythonMirror', { required: true })
|
||||
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
|
||||
const torchMirror = defineModel<string>('torchMirror', { required: true })
|
||||
|
||||
const isBlackwellArchitecture = ref(false)
|
||||
|
||||
const requiresNightlyPytorch = async (): Promise<boolean> => {
|
||||
try {
|
||||
return await electronAPI().isBlackwell()
|
||||
} catch (error) {
|
||||
console.error('Failed to detect Blackwell architecture:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
|
||||
switch (device) {
|
||||
case 'mps':
|
||||
return {
|
||||
settingId,
|
||||
mirror: NIGHTLY_CPU_TORCH_URL,
|
||||
fallbackMirror: NIGHTLY_CPU_TORCH_URL
|
||||
mirror: TorchMirrorUrl.NightlyCpu,
|
||||
fallbackMirror: TorchMirrorUrl.NightlyCpu
|
||||
}
|
||||
case 'nvidia':
|
||||
if (isBlackwellArchitecture.value) {
|
||||
return {
|
||||
settingId,
|
||||
mirror: TorchMirrorUrl.NightlyCuda,
|
||||
fallbackMirror: TorchMirrorUrl.NightlyCuda
|
||||
}
|
||||
}
|
||||
return {
|
||||
settingId,
|
||||
mirror: CUDA_TORCH_URL,
|
||||
fallbackMirror: CUDA_TORCH_URL
|
||||
mirror: TorchMirrorUrl.Cuda,
|
||||
fallbackMirror: TorchMirrorUrl.Cuda
|
||||
}
|
||||
case 'cpu':
|
||||
default:
|
||||
@@ -103,7 +82,6 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
const userIsInChina = ref(false)
|
||||
onMounted(async () => {
|
||||
userIsInChina.value = await isInChina()
|
||||
isBlackwellArchitecture.value = await requiresNightlyPytorch()
|
||||
})
|
||||
|
||||
const useFallbackMirror = (mirror: UVMirror) => ({
|
||||
|
||||
@@ -158,7 +158,10 @@ const showContextMenu = (e: CanvasPointerEvent) => {
|
||||
const commonOptions = {
|
||||
e,
|
||||
allow_searchbox: true,
|
||||
showSearchBox: () => showSearchBox(e)
|
||||
showSearchBox: () => {
|
||||
cancelResetOnContextClose()
|
||||
showSearchBox(e)
|
||||
}
|
||||
}
|
||||
const connectionOptions =
|
||||
toType === 'input'
|
||||
@@ -200,7 +203,12 @@ const showContextMenu = (e: CanvasPointerEvent) => {
|
||||
)
|
||||
|
||||
// Reset when the context menu is closed
|
||||
useEventListener(menu.controller.signal, 'abort', reset, options)
|
||||
const cancelResetOnContextClose = useEventListener(
|
||||
menu.controller.signal,
|
||||
'abort',
|
||||
reset,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
// Disable litegraph's default behavior of release link and search box.
|
||||
@@ -246,7 +254,7 @@ const cancelNextReset = (e: CustomEvent<CanvasPointerEvent>) => {
|
||||
e.preventDefault()
|
||||
|
||||
const canvas = canvasStore.getCanvas()
|
||||
canvas._highlight_pos = [e.detail.canvasX, e.detail.canvasY]
|
||||
canvas.linkConnector.state.snapLinksPos = [e.detail.canvasX, e.detail.canvasY]
|
||||
useEventListener(canvas.linkConnector.events, 'reset', preventDefault, {
|
||||
once: true
|
||||
})
|
||||
@@ -283,7 +291,6 @@ const reset = () => {
|
||||
if (disconnectOnReset) canvas.linkConnector.disconnectLinks()
|
||||
|
||||
canvas.linkConnector.reset()
|
||||
canvas._highlight_pos = undefined
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import OverlayBadge from 'primevue/overlaybadge'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SidebarIcon from '../SidebarIcon.vue'
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
type SidebarIconProps = {
|
||||
icon: string
|
||||
@@ -1,49 +1,49 @@
|
||||
<!-- A button that shows current authenticated user's avatar -->
|
||||
<template>
|
||||
<Button
|
||||
v-if="isAuthenticated"
|
||||
v-tooltip="{ value: $t('userSettings.title'), showDelay: 300 }"
|
||||
class="user-profile-button p-1"
|
||||
severity="secondary"
|
||||
text
|
||||
:aria-label="$t('userSettings.title')"
|
||||
@click="openUserSettings"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 pr-2 rounded-full bg-[var(--p-content-background)]"
|
||||
<div>
|
||||
<Button
|
||||
v-if="isAuthenticated"
|
||||
class="user-profile-button p-1"
|
||||
severity="secondary"
|
||||
text
|
||||
aria-label="user profile"
|
||||
@click="popover?.toggle($event)"
|
||||
>
|
||||
<!-- User Avatar if available -->
|
||||
<div v-if="user?.photoURL" class="flex items-center gap-1">
|
||||
<img
|
||||
:src="user.photoURL"
|
||||
:alt="user.displayName || ''"
|
||||
class="w-8 h-8 rounded-full"
|
||||
<div
|
||||
class="flex items-center rounded-full bg-[var(--p-content-background)]"
|
||||
>
|
||||
<Avatar
|
||||
:image="photoURL"
|
||||
:icon="photoURL ? undefined : 'pi pi-user'"
|
||||
shape="circle"
|
||||
aria-label="User Avatar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- User Icon if no avatar -->
|
||||
<div v-else class="w-8 h-8 rounded-full flex items-center justify-center">
|
||||
<i class="pi pi-user text-sm" />
|
||||
<i class="pi pi-chevron-down px-1" :style="{ fontSize: '0.5rem' }" />
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<i class="pi pi-chevron-down" :style="{ fontSize: '0.5rem' }" />
|
||||
</div>
|
||||
</Button>
|
||||
<Popover ref="popover" :show-arrow="false">
|
||||
<CurrentUserPopover />
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Avatar from 'primevue/avatar'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const dialogService = useDialogService()
|
||||
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||
const user = computed(() => authStore.currentUser)
|
||||
import CurrentUserPopover from './CurrentUserPopover.vue'
|
||||
|
||||
const openUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
}
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const popover = ref<InstanceType<typeof Popover> | null>(null)
|
||||
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||
const photoURL = computed<string | undefined>(
|
||||
() => authStore.currentUser?.photoURL ?? undefined
|
||||
)
|
||||
</script>
|
||||
|
||||
80
src/components/topbar/CurrentUserPopover.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<!-- A popover that shows current user information and actions -->
|
||||
<template>
|
||||
<div class="current-user-popover w-72">
|
||||
<!-- User Info Section -->
|
||||
<div class="p-3">
|
||||
<div class="flex flex-col items-center">
|
||||
<Avatar
|
||||
class="mb-3"
|
||||
:image="user?.photoURL ?? undefined"
|
||||
:icon="user?.photoURL ? undefined : 'pi pi-user'"
|
||||
shape="circle"
|
||||
size="large"
|
||||
aria-label="User Avatar"
|
||||
/>
|
||||
|
||||
<!-- User Details -->
|
||||
<h3 class="text-lg font-semibold truncate my-0 mb-1">
|
||||
{{ user?.displayName || $t('g.user') }}
|
||||
</h3>
|
||||
<p v-if="user?.email" class="text-sm text-muted truncate my-0">
|
||||
{{ user.email }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider class="my-2" />
|
||||
|
||||
<Button
|
||||
class="justify-start"
|
||||
:label="$t('userSettings.title')"
|
||||
icon="pi pi-cog"
|
||||
text
|
||||
fluid
|
||||
severity="secondary"
|
||||
@click="handleOpenUserSettings"
|
||||
/>
|
||||
|
||||
<Divider class="my-2" />
|
||||
|
||||
<div class="w-full flex flex-col gap-2 p-2">
|
||||
<div class="text-muted text-sm">
|
||||
{{ $t('credits.yourCreditBalance') }}
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<UserCredit text-class="text-2xl" />
|
||||
<Button :label="$t('credits.topUp.topUp')" @click="handleTopUp" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Avatar from 'primevue/avatar'
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authService = useFirebaseAuthService()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const user = computed(() => authStore.currentUser)
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
}
|
||||
|
||||
const handleTopUp = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void authService.fetchBalance()
|
||||
})
|
||||
</script>
|
||||
@@ -1,4 +1,10 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import {
|
||||
type Component,
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
onMounted,
|
||||
ref
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
@@ -8,6 +14,11 @@ import { isElectron } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
interface SettingPanelItem {
|
||||
node: SettingTreeNode
|
||||
component: Component
|
||||
}
|
||||
|
||||
export function useSettingUI(
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
@@ -48,49 +59,82 @@ export function useSettingUI(
|
||||
() => settingRoot.value.children ?? []
|
||||
)
|
||||
|
||||
// Define panel nodes
|
||||
const aboutPanelNode: SettingTreeNode = {
|
||||
key: 'about',
|
||||
label: 'About',
|
||||
children: []
|
||||
// Define panel items
|
||||
const aboutPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'about',
|
||||
label: 'About',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/AboutPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const creditsPanelNode: SettingTreeNode = {
|
||||
key: 'credits',
|
||||
label: 'Credits',
|
||||
children: []
|
||||
const creditsPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'credits',
|
||||
label: 'Credits',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/CreditsPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const userPanelNode: SettingTreeNode = {
|
||||
key: 'user',
|
||||
label: 'User',
|
||||
children: []
|
||||
const userPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'user',
|
||||
label: 'User',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/UserPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const keybindingPanelNode: SettingTreeNode = {
|
||||
key: 'keybinding',
|
||||
label: 'Keybinding',
|
||||
children: []
|
||||
const keybindingPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'keybinding',
|
||||
label: 'Keybinding',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/KeybindingPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const extensionPanelNode: SettingTreeNode = {
|
||||
key: 'extension',
|
||||
label: 'Extension',
|
||||
children: []
|
||||
const extensionPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'extension',
|
||||
label: 'Extension',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/ExtensionPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const serverConfigPanelNode: SettingTreeNode = {
|
||||
key: 'server-config',
|
||||
label: 'Server-Config',
|
||||
children: []
|
||||
const serverConfigPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'server-config',
|
||||
label: 'Server-Config',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/ServerConfigPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Server config panel is only available in Electron
|
||||
*/
|
||||
const serverConfigPanelNodeList = computed<SettingTreeNode[]>(() => {
|
||||
return isElectron() ? [serverConfigPanelNode] : []
|
||||
})
|
||||
const panels = computed<SettingPanelItem[]>(() =>
|
||||
[
|
||||
aboutPanel,
|
||||
creditsPanel,
|
||||
userPanel,
|
||||
keybindingPanel,
|
||||
extensionPanel
|
||||
].filter((panel) => panel.component)
|
||||
)
|
||||
|
||||
/**
|
||||
* The default category to show when the dialog is opened.
|
||||
@@ -119,8 +163,8 @@ export function useSettingUI(
|
||||
key: 'account',
|
||||
label: 'Account',
|
||||
children: [
|
||||
userPanelNode,
|
||||
...(firebaseAuthStore.isAuthenticated ? [creditsPanelNode] : [])
|
||||
userPanel.node,
|
||||
...(firebaseAuthStore.isAuthenticated ? [creditsPanel.node] : [])
|
||||
].map(translateCategory)
|
||||
},
|
||||
// Normal settings stored in the settingStore
|
||||
@@ -134,10 +178,10 @@ export function useSettingUI(
|
||||
key: 'specialSettings',
|
||||
label: 'Special Settings',
|
||||
children: [
|
||||
keybindingPanelNode,
|
||||
extensionPanelNode,
|
||||
aboutPanelNode,
|
||||
...serverConfigPanelNodeList.value
|
||||
keybindingPanel.node,
|
||||
extensionPanel.node,
|
||||
aboutPanel.node,
|
||||
...(isElectron() ? [serverConfigPanel.node] : [])
|
||||
].map(translateCategory)
|
||||
}
|
||||
])
|
||||
@@ -147,6 +191,7 @@ export function useSettingUI(
|
||||
})
|
||||
|
||||
return {
|
||||
panels,
|
||||
activeCategory,
|
||||
defaultCategory,
|
||||
groupedMenuTreeNodes,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
@@ -31,6 +32,8 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const dialogService = useDialogService()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const firebaseAuthService = useFirebaseAuthService()
|
||||
const toastStore = useToastStore()
|
||||
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
||||
|
||||
const getSelectedNodes = (): LGraphNode[] => {
|
||||
@@ -55,8 +58,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
})
|
||||
}
|
||||
|
||||
const commonProps = { source: 'System' }
|
||||
|
||||
const commands = [
|
||||
{
|
||||
id: 'Comfy.NewBlankWorkflow',
|
||||
@@ -184,7 +185,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Interrupt',
|
||||
function: async () => {
|
||||
await api.interrupt()
|
||||
useToastStore().add({
|
||||
toastStore.add({
|
||||
severity: 'info',
|
||||
summary: t('g.interrupted'),
|
||||
detail: t('toastMessages.interrupted'),
|
||||
@@ -198,7 +199,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Clear Pending Tasks',
|
||||
function: async () => {
|
||||
await useQueueStore().clear(['queue'])
|
||||
useToastStore().add({
|
||||
toastStore.add({
|
||||
severity: 'info',
|
||||
summary: t('g.confirmed'),
|
||||
detail: t('toastMessages.pendingTasksDeleted'),
|
||||
@@ -246,7 +247,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Fit view to selected nodes',
|
||||
function: () => {
|
||||
if (app.canvas.empty) {
|
||||
useToastStore().add({
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.emptyCanvas'),
|
||||
life: 3000
|
||||
@@ -328,7 +329,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
const { canvas } = app
|
||||
if (!canvas.selectedItems?.size) {
|
||||
useToastStore().add({
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.nothingToGroup'),
|
||||
detail: t('toastMessages.pleaseSelectNodesToGroup'),
|
||||
@@ -641,8 +642,17 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: async () => {
|
||||
await dialogService.showSignInDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.User.SignOut',
|
||||
icon: 'pi pi-sign-out',
|
||||
label: 'Sign Out',
|
||||
versionAdded: '1.18.1',
|
||||
function: async () => {
|
||||
await firebaseAuthService.logout()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return commands.map((command) => ({ ...command, ...commonProps }))
|
||||
return commands.map((command) => ({ ...command, source: 'System' }))
|
||||
}
|
||||
|
||||
@@ -88,6 +88,14 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
// Default to small if the window is less than 1536px(2xl) wide.
|
||||
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Sidebar.UnifiedWidth',
|
||||
category: ['Appearance', 'Sidebar', 'UnifiedWidth'],
|
||||
name: 'Unified sidebar width',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.18.1'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TextareaWidget.FontSize',
|
||||
category: ['Appearance', 'Node Widget', 'TextareaWidget', 'FontSize'],
|
||||
@@ -794,5 +802,14 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.16.1'
|
||||
},
|
||||
{
|
||||
id: 'LiteGraph.Node.DefaultPadding',
|
||||
name: 'Always shrink new nodes',
|
||||
tooltip:
|
||||
'Resize nodes to the smallest possible size when created. When disabled, a newly added node will be widened slightly to show widget values.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.18.0'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,6 +10,7 @@ import './load3d'
|
||||
import './maskeditor'
|
||||
import './nodeTemplates'
|
||||
import './noteNode'
|
||||
import './previewAny'
|
||||
import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
import './saveMesh'
|
||||
|
||||
50
src/extensions/core/previewAny.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Preview Any - original implement from
|
||||
https://github.com/rgthree/rgthree-comfy/blob/main/py/display_any.py
|
||||
upstream requested in https://github.com/Kosinkadink/rfcs/blob/main/rfcs/0000-corenodes.md#preview-nodes
|
||||
*/
|
||||
import { IWidget } from '@comfyorg/litegraph'
|
||||
|
||||
import { DOMWidget } from '@/scripts/domWidget'
|
||||
import { ComfyWidgets } from '@/scripts/widgets'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.PreviewAny',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name === 'PreviewAny') {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
onNodeCreated ? onNodeCreated.apply(this, []) : undefined
|
||||
|
||||
const showValueWidget = ComfyWidgets['STRING'](
|
||||
this,
|
||||
'preview',
|
||||
['STRING', { multiline: true }],
|
||||
app
|
||||
).widget as DOMWidget<any, any>
|
||||
|
||||
showValueWidget.element.readOnly = true
|
||||
|
||||
console.log(showValueWidget)
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted === null || onExecuted === void 0
|
||||
? void 0
|
||||
: onExecuted.apply(this, [message])
|
||||
|
||||
const previewWidget = this.widgets?.find(
|
||||
(w: IWidget) => w.name === 'preview'
|
||||
)
|
||||
|
||||
if (previewWidget) {
|
||||
previewWidget.value = message.text[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2,42 +2,35 @@ import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let touchZooming
|
||||
let touchZooming = false
|
||||
let touchCount = 0
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.SimpleTouchSupport',
|
||||
setup() {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let touchDist
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let touchTime
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let lastTouch
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let lastScale
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function getMultiTouchPos(e) {
|
||||
let touchDist: number | null = null
|
||||
let touchTime: Date | null = null
|
||||
let lastTouch: { clientX: number; clientY: number } | null = null
|
||||
let lastScale: number | null = null
|
||||
function getMultiTouchPos(e: TouchEvent) {
|
||||
return Math.hypot(
|
||||
e.touches[0].clientX - e.touches[1].clientX,
|
||||
e.touches[0].clientY - e.touches[1].clientY
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function getMultiTouchCenter(e) {
|
||||
function getMultiTouchCenter(e: TouchEvent) {
|
||||
return {
|
||||
clientX: (e.touches[0].clientX + e.touches[1].clientX) / 2,
|
||||
clientY: (e.touches[0].clientY + e.touches[1].clientY) / 2
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
app.canvasEl.parentElement.addEventListener(
|
||||
app.canvasEl.parentElement?.addEventListener(
|
||||
'touchstart',
|
||||
(e: TouchEvent) => {
|
||||
touchCount++
|
||||
touchCount += e.changedTouches.length
|
||||
|
||||
lastTouch = null
|
||||
lastScale = null
|
||||
if (e.touches?.length === 1) {
|
||||
@@ -59,35 +52,48 @@ app.registerExtension({
|
||||
true
|
||||
)
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
app.canvasEl.parentElement.addEventListener('touchend', (e: TouchEvent) => {
|
||||
touchCount--
|
||||
app.canvasEl.parentElement?.addEventListener(
|
||||
'touchend',
|
||||
(e: TouchEvent) => {
|
||||
touchCount -= e.changedTouches.length
|
||||
|
||||
if (e.touches?.length !== 1) touchZooming = false
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (touchTime && !e.touches?.length) {
|
||||
if (new Date().getTime() - touchTime > 600) {
|
||||
if (e.target === app.canvasEl) {
|
||||
app.canvasEl.dispatchEvent(
|
||||
new PointerEvent('pointerdown', {
|
||||
button: 2,
|
||||
if (e.touches?.length !== 1) touchZooming = false
|
||||
if (touchTime && !e.touches?.length) {
|
||||
if (new Date().getTime() - touchTime.getTime() > 600) {
|
||||
if (e.target === app.canvasEl) {
|
||||
const touch = {
|
||||
button: 2, // Right click
|
||||
clientX: e.changedTouches[0].clientX,
|
||||
clientY: e.changedTouches[0].clientY
|
||||
clientY: e.changedTouches[0].clientY,
|
||||
pointerId: 1, // changedTouches' id is 0, set it to any number
|
||||
isPrimary: true // changedTouches' isPrimary is false, so set it to true
|
||||
}
|
||||
// context menu info set in 'pointerdown' event
|
||||
app.canvasEl.dispatchEvent(new PointerEvent('pointerdown', touch))
|
||||
// then, context menu open after 'pointerup' event
|
||||
setTimeout(() => {
|
||||
app.canvasEl.dispatchEvent(new PointerEvent('pointerup', touch))
|
||||
})
|
||||
)
|
||||
e.preventDefault()
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
touchTime = null
|
||||
}
|
||||
touchTime = null
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
app.canvasEl.parentElement.addEventListener(
|
||||
app.canvasEl.parentElement?.addEventListener(
|
||||
'touchmove',
|
||||
(e) => {
|
||||
touchTime = null
|
||||
// @ts-expect-error fixme ts strict error
|
||||
// make a threshold for touchmove to prevent clear touchTime for long press
|
||||
if (touchTime && lastTouch && e.touches?.length === 1) {
|
||||
const onlyTouch = e.touches[0]
|
||||
const deltaX = onlyTouch.clientX - lastTouch.clientX
|
||||
const deltaY = onlyTouch.clientY - lastTouch.clientY
|
||||
if (deltaX * deltaX + deltaY * deltaY > 30) {
|
||||
touchTime = null
|
||||
}
|
||||
}
|
||||
if (e.touches?.length === 2 && lastTouch && !e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault() // Prevent browser from zooming when two textareas are touched
|
||||
app.canvas.pointer.isDown = false
|
||||
@@ -100,7 +106,7 @@ app.registerExtension({
|
||||
|
||||
const center = getMultiTouchCenter(e)
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (lastScale === null || touchDist === null) return
|
||||
let scale = (lastScale * newTouchDist) / touchDist
|
||||
|
||||
const newX = (center.clientX - lastTouch.clientX) / scale
|
||||
@@ -124,8 +130,7 @@ app.registerExtension({
|
||||
|
||||
const newScale = app.canvas.ds.scale
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const convertScaleToOffset = (scale) => [
|
||||
const convertScaleToOffset = (scale: number) => [
|
||||
center.clientX / scale - app.canvas.ds.offset[0],
|
||||
center.clientY / scale - app.canvas.ds.offset[1]
|
||||
]
|
||||
@@ -147,22 +152,18 @@ app.registerExtension({
|
||||
})
|
||||
|
||||
const processMouseDown = LGraphCanvas.prototype.processMouseDown
|
||||
LGraphCanvas.prototype.processMouseDown = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
LGraphCanvas.prototype.processMouseDown = function (e: PointerEvent) {
|
||||
if (touchZooming || touchCount) {
|
||||
return
|
||||
}
|
||||
app.canvas.pointer.isDown = false // Prevent context menu from opening on second tap
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return processMouseDown.apply(this, arguments)
|
||||
return processMouseDown.apply(this, [e])
|
||||
}
|
||||
|
||||
const processMouseMove = LGraphCanvas.prototype.processMouseMove
|
||||
LGraphCanvas.prototype.processMouseMove = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
LGraphCanvas.prototype.processMouseMove = function (e: PointerEvent) {
|
||||
if (touchZooming || touchCount > 1) {
|
||||
return
|
||||
}
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return processMouseMove.apply(this, arguments)
|
||||
return processMouseMove.apply(this, [e])
|
||||
}
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Open Sign In Dialog"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Sign Out"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Close Current Workflow"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"g": {
|
||||
"user": "User",
|
||||
"currentUser": "Current user",
|
||||
"empty": "Empty",
|
||||
"noWorkflowsFound": "No workflows found.",
|
||||
@@ -111,7 +112,8 @@
|
||||
"updateAvailable": "Update Available",
|
||||
"login": "Login",
|
||||
"learnMore": "Learn more",
|
||||
"amount": "Amount"
|
||||
"amount": "Amount",
|
||||
"unknownError": "Unknown error"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -678,6 +680,7 @@
|
||||
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
|
||||
"Undo": "Undo",
|
||||
"Open Sign In Dialog": "Open Sign In Dialog",
|
||||
"Sign Out": "Sign Out",
|
||||
"Close Current Workflow": "Close Current Workflow",
|
||||
"Next Opened Workflow": "Next Opened Workflow",
|
||||
"Previous Opened Workflow": "Previous Opened Workflow",
|
||||
@@ -744,7 +747,9 @@
|
||||
"Camera": "Camera",
|
||||
"Scene": "Scene",
|
||||
"3D": "3D",
|
||||
"Light": "Light"
|
||||
"Light": "Light",
|
||||
"User": "User",
|
||||
"Credits": "Credits"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -1074,12 +1079,22 @@
|
||||
"errorCopyImage": "Error copying image: {error}",
|
||||
"noTemplatesToExport": "No templates to export",
|
||||
"failedToFetchLogs": "Failed to fetch server logs",
|
||||
"migrateToLitegraphReroute": "Reroute nodes will be removed in future versions. Click to migrate to litegraph-native reroute."
|
||||
"migrateToLitegraphReroute": "Reroute nodes will be removed in future versions. Click to migrate to litegraph-native reroute.",
|
||||
"userNotAuthenticated": "User not authenticated",
|
||||
"firebaseAuthNotInitialized": "Firebase Auth not initialized",
|
||||
"failedToFetchBalance": "Failed to fetch balance: {error}",
|
||||
"failedToCreateCustomer": "Failed to create customer: {error}",
|
||||
"failedToInitiateCreditPurchase": "Failed to initiate credit purchase: {error}",
|
||||
"failedToAccessBillingPortal": "Failed to access billing portal: {error}",
|
||||
"failedToPurchaseCredits": "Failed to purchase credits: {error}"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"title": "Log in to your account",
|
||||
"signInOrSignUp": "Sign In / Sign Up",
|
||||
"forgotPasswordError": "Failed to send password reset email",
|
||||
"passwordResetSent": "Password reset email sent",
|
||||
"passwordResetSentDetail": "Please check your email for a link to reset your password.",
|
||||
"newUser": "New here?",
|
||||
"signUp": "Sign up",
|
||||
"emailLabel": "Email",
|
||||
@@ -1098,8 +1113,7 @@
|
||||
"andText": "and",
|
||||
"privacyLink": "Privacy Policy",
|
||||
"success": "Login successful",
|
||||
"failed": "Login failed",
|
||||
"genericErrorMessage": "Sorry, we've encountered an error. Please contact {supportEmail}."
|
||||
"failed": "Login failed"
|
||||
},
|
||||
"signup": {
|
||||
"title": "Create an account",
|
||||
@@ -1117,20 +1131,6 @@
|
||||
"signOut": "Log Out",
|
||||
"success": "Signed out successfully",
|
||||
"successDetail": "You have been signed out of your account."
|
||||
},
|
||||
"required": {
|
||||
"signIn": {
|
||||
"title": "Sign-In Required to Execute Workflow",
|
||||
"message": "This workflow includes nodes that require an active account. Please log in or create one to continue.",
|
||||
"hint": "To login go to: Settings > User > Login",
|
||||
"action": "Open Settings to Login"
|
||||
},
|
||||
"credits": {
|
||||
"title": "Credits Required to Execute Workflow",
|
||||
"message": "This workflow includes nodes that require credits. Please add credits to your account to continue.",
|
||||
"hint": "To add credits go to: Settings > User > Credits",
|
||||
"action": "Open Settings to Add Credits"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
@@ -1152,17 +1152,18 @@
|
||||
"credits": "Credits",
|
||||
"yourCreditBalance": "Your credit balance",
|
||||
"purchaseCredits": "Purchase Credits",
|
||||
"creditsHistory": "Credits History",
|
||||
"invoiceHistory": "Invoice History",
|
||||
"faqs": "FAQs",
|
||||
"messageSupport": "Message Support",
|
||||
"lastUpdated": "Last updated",
|
||||
"topUp": {
|
||||
"title": "Add to Credit Balance",
|
||||
"insufficientTitle": "Insufficient Credits",
|
||||
"insufficientMessage": "You don't have enough credits to run this workflow.",
|
||||
"addCredits": "Add credits to your balance",
|
||||
"quickPurchase": "Quick Purchase",
|
||||
"maxAmount": "(Max. $1,000 USD)",
|
||||
"buyNow": "Buy now"
|
||||
"buyNow": "Buy now",
|
||||
"seeDetails": "See details",
|
||||
"topUp": "Top Up"
|
||||
}
|
||||
},
|
||||
"userSettings": {
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "small"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Unified sidebar width"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Snap to grid size",
|
||||
"tooltip": "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Scale node combo widget menus (lists) when zoomed in"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Always shrink new nodes",
|
||||
"tooltip": "Resize nodes to the smallest possible size when created. When disabled, a newly added node will be widened slightly to show widget values."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Tooltip Delay"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Abrir diálogo de inicio de sesión"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Cerrar sesión"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Cerrar Flujo de Trabajo Actual"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"emailPlaceholder": "Ingresa tu correo electrónico",
|
||||
"failed": "Inicio de sesión fallido",
|
||||
"forgotPassword": "¿Olvidaste tu contraseña?",
|
||||
"genericErrorMessage": "Lo sentimos, hemos encontrado un error. Por favor, contacta con {supportEmail}.",
|
||||
"forgotPasswordError": "No se pudo enviar el correo electrónico para restablecer la contraseña",
|
||||
"loginButton": "Iniciar sesión",
|
||||
"loginWithGithub": "Iniciar sesión con Github",
|
||||
"loginWithGoogle": "Iniciar sesión con Google",
|
||||
@@ -25,6 +25,8 @@
|
||||
"orContinueWith": "O continuar con",
|
||||
"passwordLabel": "Contraseña",
|
||||
"passwordPlaceholder": "Ingresa tu contraseña",
|
||||
"passwordResetSent": "Correo electrónico de restablecimiento de contraseña enviado",
|
||||
"passwordResetSentDetail": "Por favor, revisa tu correo electrónico para encontrar un enlace para restablecer tu contraseña.",
|
||||
"privacyLink": "Política de privacidad",
|
||||
"signInOrSignUp": "Iniciar sesión / Registrarse",
|
||||
"signUp": "Regístrate",
|
||||
@@ -33,20 +35,6 @@
|
||||
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
|
||||
"title": "Inicia sesión en tu cuenta"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "Abrir configuración para añadir créditos",
|
||||
"hint": "Para añadir créditos ve a: Configuración > Usuario > Créditos",
|
||||
"message": "Este flujo de trabajo incluye nodos que requieren créditos. Por favor, añade créditos a tu cuenta para continuar.",
|
||||
"title": "Créditos requeridos para ejecutar el flujo de trabajo"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "Abrir configuración para iniciar sesión",
|
||||
"hint": "Para iniciar sesión ve a: Configuración > Usuario > Iniciar sesión",
|
||||
"message": "Este flujo de trabajo incluye nodos que requieren una cuenta activa. Por favor, inicia sesión o crea una para continuar.",
|
||||
"title": "Inicio de sesión requerido para ejecutar el flujo de trabajo"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Cerrar sesión",
|
||||
"success": "Sesión cerrada correctamente",
|
||||
@@ -115,18 +103,19 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Créditos",
|
||||
"creditsHistory": "Historial de créditos",
|
||||
"faqs": "Preguntas frecuentes",
|
||||
"invoiceHistory": "Historial de facturas",
|
||||
"lastUpdated": "Última actualización",
|
||||
"messageSupport": "Contactar soporte",
|
||||
"purchaseCredits": "Comprar créditos",
|
||||
"topUp": {
|
||||
"addCredits": "Agregar créditos a tu saldo",
|
||||
"buyNow": "Comprar ahora",
|
||||
"insufficientMessage": "No tienes suficientes créditos para ejecutar este flujo de trabajo.",
|
||||
"insufficientTitle": "Créditos insuficientes",
|
||||
"maxAmount": "(Máx. $1,000 USD)",
|
||||
"title": "Agregar al saldo de créditos"
|
||||
"quickPurchase": "Compra rápida",
|
||||
"seeDetails": "Ver detalles",
|
||||
"topUp": "Recargar"
|
||||
},
|
||||
"yourCreditBalance": "Tu saldo de créditos"
|
||||
},
|
||||
@@ -308,11 +297,13 @@
|
||||
"success": "Éxito",
|
||||
"systemInfo": "Información del sistema",
|
||||
"terminal": "Terminal",
|
||||
"unknownError": "Error desconocido",
|
||||
"update": "Actualizar",
|
||||
"updateAvailable": "Actualización Disponible",
|
||||
"updated": "Actualizado",
|
||||
"updating": "Actualizando",
|
||||
"upload": "Subir",
|
||||
"user": "Usuario",
|
||||
"videoFailedToLoad": "Falló la carga del video",
|
||||
"workflow": "Flujo de trabajo"
|
||||
},
|
||||
@@ -675,6 +666,7 @@
|
||||
"Save": "Guardar",
|
||||
"Save As": "Guardar como",
|
||||
"Show Settings Dialog": "Mostrar diálogo de configuración",
|
||||
"Sign Out": "Cerrar sesión",
|
||||
"Toggle Bottom Panel": "Alternar panel inferior",
|
||||
"Toggle Focus Mode": "Alternar modo de enfoque",
|
||||
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
|
||||
@@ -928,6 +920,7 @@
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfy-Desktop",
|
||||
"ContextMenu": "Menú Contextual",
|
||||
"Credits": "Créditos",
|
||||
"CustomColorPalettes": "Paletas de Colores Personalizadas",
|
||||
"DevMode": "Modo de Desarrollo",
|
||||
"EditTokenWeight": "Editar Peso del Token",
|
||||
@@ -962,6 +955,7 @@
|
||||
"Sidebar": "Barra Lateral",
|
||||
"Tree Explorer": "Explorador de Árbol",
|
||||
"UV": "UV",
|
||||
"User": "Usuario",
|
||||
"Validation": "Validación",
|
||||
"Window": "Ventana",
|
||||
"Workflow": "Flujo de Trabajo"
|
||||
@@ -1116,12 +1110,18 @@
|
||||
"errorCopyImage": "Error al copiar la imagen: {error}",
|
||||
"errorLoadingModel": "Error al cargar el modelo",
|
||||
"errorSaveSetting": "Error al guardar la configuración {id}: {err}",
|
||||
"failedToAccessBillingPortal": "No se pudo acceder al portal de facturación: {error}",
|
||||
"failedToApplyTexture": "Error al aplicar textura",
|
||||
"failedToCreateCustomer": "No se pudo crear el cliente: {error}",
|
||||
"failedToDownloadFile": "Error al descargar el archivo",
|
||||
"failedToExportModel": "Error al exportar modelo como {format}",
|
||||
"failedToFetchBalance": "No se pudo obtener el saldo: {error}",
|
||||
"failedToFetchLogs": "Error al obtener los registros del servidor",
|
||||
"failedToInitiateCreditPurchase": "No se pudo iniciar la compra de créditos: {error}",
|
||||
"failedToPurchaseCredits": "No se pudo comprar créditos: {error}",
|
||||
"fileLoadError": "No se puede encontrar el flujo de trabajo en {fileName}",
|
||||
"fileUploadFailed": "Error al subir el archivo",
|
||||
"firebaseAuthNotInitialized": "Firebase Auth no está inicializado",
|
||||
"interrupted": "La ejecución ha sido interrumpida",
|
||||
"migrateToLitegraphReroute": "Los nodos de reroute se eliminarán en futuras versiones. Haz clic para migrar a reroute nativo de litegraph.",
|
||||
"no3dScene": "No hay escena 3D para aplicar textura",
|
||||
@@ -1132,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "Tareas pendientes eliminadas",
|
||||
"pleaseSelectNodesToGroup": "Por favor, seleccione los nodos (u otros grupos) para crear un grupo para",
|
||||
"unableToGetModelFilePath": "No se puede obtener la ruta del archivo del modelo",
|
||||
"updateRequested": "Actualización solicitada"
|
||||
"updateRequested": "Actualización solicitada",
|
||||
"userNotAuthenticated": "Usuario no autenticado"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "Introduce un nombre de usuario",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "pequeña"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Ancho unificado de la barra lateral"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Tamaño de la cuadrícula para ajustar",
|
||||
"tooltip": "Al arrastrar y redimensionar nodos mientras se mantiene presionada la tecla shift, se alinearán a la cuadrícula, esto controla el tamaño de esa cuadrícula."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Escala los menús de widgets combinados de nodos (listas) al acercar"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Reducir siempre los nuevos nodos",
|
||||
"tooltip": "Redimensiona los nodos al tamaño más pequeño posible al crearlos. Si está desactivado, un nodo recién añadido se ampliará ligeramente para mostrar los valores de los widgets."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Retraso de la información sobre herramientas"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Ouvrir la boîte de dialogue de connexion"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Se déconnecter"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Fermer le flux de travail actuel"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"emailPlaceholder": "Entrez votre email",
|
||||
"failed": "Échec de la connexion",
|
||||
"forgotPassword": "Mot de passe oublié?",
|
||||
"genericErrorMessage": "Désolé, une erreur s'est produite. Veuillez contacter {supportEmail}.",
|
||||
"forgotPasswordError": "Échec de l'envoi de l'e-mail de réinitialisation du mot de passe",
|
||||
"loginButton": "Se connecter",
|
||||
"loginWithGithub": "Se connecter avec Github",
|
||||
"loginWithGoogle": "Se connecter avec Google",
|
||||
@@ -25,6 +25,8 @@
|
||||
"orContinueWith": "Ou continuer avec",
|
||||
"passwordLabel": "Mot de passe",
|
||||
"passwordPlaceholder": "Entrez votre mot de passe",
|
||||
"passwordResetSent": "E-mail de réinitialisation du mot de passe envoyé",
|
||||
"passwordResetSentDetail": "Veuillez vérifier votre e-mail pour un lien de réinitialisation de votre mot de passe.",
|
||||
"privacyLink": "Politique de confidentialité",
|
||||
"signInOrSignUp": "Se connecter / S’inscrire",
|
||||
"signUp": "S'inscrire",
|
||||
@@ -33,20 +35,6 @@
|
||||
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
|
||||
"title": "Connectez-vous à votre compte"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "Ouvrir les paramètres pour ajouter des crédits",
|
||||
"hint": "Pour ajouter des crédits, allez dans : Paramètres > Utilisateur > Crédits",
|
||||
"message": "Ce workflow inclut des nœuds nécessitant des crédits. Veuillez ajouter des crédits à votre compte pour continuer.",
|
||||
"title": "Crédits requis pour exécuter le workflow"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "Ouvrir les paramètres pour se connecter",
|
||||
"hint": "Pour vous connecter, allez dans : Paramètres > Utilisateur > Connexion",
|
||||
"message": "Ce workflow inclut des nœuds nécessitant un compte actif. Veuillez vous connecter ou en créer un pour continuer.",
|
||||
"title": "Connexion requise pour exécuter le workflow"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Se déconnecter",
|
||||
"success": "Déconnexion réussie",
|
||||
@@ -115,18 +103,19 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Crédits",
|
||||
"creditsHistory": "Historique des crédits",
|
||||
"faqs": "FAQ",
|
||||
"invoiceHistory": "Historique des factures",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"messageSupport": "Contacter le support",
|
||||
"purchaseCredits": "Acheter des crédits",
|
||||
"topUp": {
|
||||
"addCredits": "Ajouter des crédits à votre solde",
|
||||
"buyNow": "Acheter maintenant",
|
||||
"insufficientMessage": "Vous n'avez pas assez de crédits pour exécuter ce workflow.",
|
||||
"insufficientTitle": "Crédits insuffisants",
|
||||
"maxAmount": "(Max. 1 000 $ US)",
|
||||
"title": "Ajouter au solde de crédits"
|
||||
"quickPurchase": "Achat rapide",
|
||||
"seeDetails": "Voir les détails",
|
||||
"topUp": "Recharger"
|
||||
},
|
||||
"yourCreditBalance": "Votre solde de crédits"
|
||||
},
|
||||
@@ -308,11 +297,13 @@
|
||||
"success": "Succès",
|
||||
"systemInfo": "Informations système",
|
||||
"terminal": "Terminal",
|
||||
"unknownError": "Erreur inconnue",
|
||||
"update": "Mettre à jour",
|
||||
"updateAvailable": "Mise à jour disponible",
|
||||
"updated": "Mis à jour",
|
||||
"updating": "Mise à jour",
|
||||
"upload": "Téléverser",
|
||||
"user": "Utilisateur",
|
||||
"videoFailedToLoad": "Échec du chargement de la vidéo",
|
||||
"workflow": "Flux de travail"
|
||||
},
|
||||
@@ -675,6 +666,7 @@
|
||||
"Save": "Enregistrer",
|
||||
"Save As": "Enregistrer sous",
|
||||
"Show Settings Dialog": "Afficher la boîte de dialogue des paramètres",
|
||||
"Sign Out": "Se déconnecter",
|
||||
"Toggle Bottom Panel": "Basculer le panneau inférieur",
|
||||
"Toggle Focus Mode": "Basculer le mode focus",
|
||||
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
|
||||
@@ -928,6 +920,7 @@
|
||||
"Comfy": "Confort",
|
||||
"Comfy-Desktop": "Comfy-Desktop",
|
||||
"ContextMenu": "Menu Contextuel",
|
||||
"Credits": "Crédits",
|
||||
"CustomColorPalettes": "Palettes de Couleurs Personnalisées",
|
||||
"DevMode": "Mode Développeur",
|
||||
"EditTokenWeight": "Modifier le Poids du Jeton",
|
||||
@@ -962,6 +955,7 @@
|
||||
"Sidebar": "Barre Latérale",
|
||||
"Tree Explorer": "Explorateur d'Arbre",
|
||||
"UV": "UV",
|
||||
"User": "Utilisateur",
|
||||
"Validation": "Validation",
|
||||
"Window": "Fenêtre",
|
||||
"Workflow": "Flux de Travail"
|
||||
@@ -1116,12 +1110,18 @@
|
||||
"errorCopyImage": "Erreur lors de la copie de l'image: {error}",
|
||||
"errorLoadingModel": "Erreur lors du chargement du modèle",
|
||||
"errorSaveSetting": "Erreur lors de l'enregistrement du paramètre {id}: {err}",
|
||||
"failedToAccessBillingPortal": "Échec de l'accès au portail de facturation : {error}",
|
||||
"failedToApplyTexture": "Échec de l'application de la texture",
|
||||
"failedToCreateCustomer": "Échec de la création du client : {error}",
|
||||
"failedToDownloadFile": "Échec du téléchargement du fichier",
|
||||
"failedToExportModel": "Échec de l'exportation du modèle en {format}",
|
||||
"failedToFetchBalance": "Échec de la récupération du solde : {error}",
|
||||
"failedToFetchLogs": "Échec de la récupération des journaux du serveur",
|
||||
"failedToInitiateCreditPurchase": "Échec de l'initiation de l'achat de crédits : {error}",
|
||||
"failedToPurchaseCredits": "Échec de l'achat de crédits : {error}",
|
||||
"fileLoadError": "Impossible de trouver le flux de travail dans {fileName}",
|
||||
"fileUploadFailed": "Échec du téléchargement du fichier",
|
||||
"firebaseAuthNotInitialized": "Authentification Firebase non initialisée",
|
||||
"interrupted": "L'exécution a été interrompue",
|
||||
"migrateToLitegraphReroute": "Les nœuds de reroute seront supprimés dans les futures versions. Cliquez pour migrer vers le reroute natif de litegraph.",
|
||||
"no3dScene": "Aucune scène 3D pour appliquer la texture",
|
||||
@@ -1132,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "Tâches en attente supprimées",
|
||||
"pleaseSelectNodesToGroup": "Veuillez sélectionner les nœuds (ou autres groupes) pour créer un groupe pour",
|
||||
"unableToGetModelFilePath": "Impossible d'obtenir le chemin du fichier modèle",
|
||||
"updateRequested": "Mise à jour demandée"
|
||||
"updateRequested": "Mise à jour demandée",
|
||||
"userNotAuthenticated": "Utilisateur non authentifié"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "Entrez un nom d'utilisateur",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "petit"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Largeur unifiée de la barre latérale"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Taille de la grille d'alignement",
|
||||
"tooltip": "Lors du déplacement et du redimensionnement des nœuds tout en maintenant shift, ils seront alignés sur la grille, cela contrôle la taille de cette grille."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Mise à l'échelle des menus de widgets combinés de nœuds (listes) lors du zoom"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Toujours réduire les nouveaux nœuds",
|
||||
"tooltip": "Redimensionner les nœuds à la taille minimale possible lors de leur création. Lorsque cette option est désactivée, un nœud nouvellement ajouté sera légèrement élargi pour afficher les valeurs des widgets."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Délai d'infobulle"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "サインインダイアログを開く"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "サインアウト"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "現在のワークフローを閉じる"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"emailPlaceholder": "メールアドレスを入力してください",
|
||||
"failed": "ログイン失敗",
|
||||
"forgotPassword": "パスワードを忘れましたか?",
|
||||
"genericErrorMessage": "申し訳ありませんが、エラーが発生しました。{supportEmail} までご連絡ください。",
|
||||
"forgotPasswordError": "パスワードリセット用メールの送信に失敗しました",
|
||||
"loginButton": "ログイン",
|
||||
"loginWithGithub": "Githubでログイン",
|
||||
"loginWithGoogle": "Googleでログイン",
|
||||
@@ -25,6 +25,8 @@
|
||||
"orContinueWith": "または以下で続ける",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholder": "パスワードを入力してください",
|
||||
"passwordResetSent": "パスワードリセット用メールを送信しました",
|
||||
"passwordResetSentDetail": "パスワードをリセットするためのリンクが記載されたメールをご確認ください。",
|
||||
"privacyLink": "プライバシーポリシー",
|
||||
"signInOrSignUp": "サインイン / サインアップ",
|
||||
"signUp": "サインアップ",
|
||||
@@ -33,20 +35,6 @@
|
||||
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
|
||||
"title": "アカウントにログインする"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "設定を開いてクレジットを追加",
|
||||
"hint": "クレジットを追加するには: 設定 > ユーザー > クレジット",
|
||||
"message": "このワークフローにはクレジットが必要なノードが含まれています。続行するにはアカウントにクレジットを追加してください。",
|
||||
"title": "ワークフロー実行にはクレジットが必要です"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "設定を開いてログイン",
|
||||
"hint": "ログインするには: 設定 > ユーザー > ログイン",
|
||||
"message": "このワークフローにはアクティブなアカウントが必要なノードが含まれています。続行するにはログインまたはアカウントを作成してください。",
|
||||
"title": "ワークフロー実行にはサインインが必要です"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "ログアウト",
|
||||
"success": "正常にサインアウトしました",
|
||||
@@ -115,18 +103,19 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "クレジット",
|
||||
"creditsHistory": "クレジット履歴",
|
||||
"faqs": "よくある質問",
|
||||
"invoiceHistory": "請求履歴",
|
||||
"lastUpdated": "最終更新",
|
||||
"messageSupport": "サポートにメッセージ",
|
||||
"purchaseCredits": "クレジットを購入",
|
||||
"topUp": {
|
||||
"addCredits": "残高にクレジットを追加",
|
||||
"buyNow": "今すぐ購入",
|
||||
"insufficientMessage": "このワークフローを実行するのに十分なクレジットがありません。",
|
||||
"insufficientTitle": "クレジット不足",
|
||||
"maxAmount": "(最大 $1,000 USD)",
|
||||
"title": "クレジット残高を追加"
|
||||
"quickPurchase": "クイック購入",
|
||||
"seeDetails": "詳細を見る",
|
||||
"topUp": "チャージ"
|
||||
},
|
||||
"yourCreditBalance": "あなたのクレジット残高"
|
||||
},
|
||||
@@ -308,11 +297,13 @@
|
||||
"success": "成功",
|
||||
"systemInfo": "システム情報",
|
||||
"terminal": "ターミナル",
|
||||
"unknownError": "不明なエラー",
|
||||
"update": "更新",
|
||||
"updateAvailable": "更新が利用可能",
|
||||
"updated": "更新済み",
|
||||
"updating": "更新中",
|
||||
"upload": "アップロード",
|
||||
"user": "ユーザー",
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
|
||||
"workflow": "ワークフロー"
|
||||
},
|
||||
@@ -675,6 +666,7 @@
|
||||
"Save": "保存",
|
||||
"Save As": "名前を付けて保存",
|
||||
"Show Settings Dialog": "設定ダイアログを表示",
|
||||
"Sign Out": "サインアウト",
|
||||
"Toggle Bottom Panel": "下部パネルの切り替え",
|
||||
"Toggle Focus Mode": "フォーカスモードの切り替え",
|
||||
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
|
||||
@@ -928,6 +920,7 @@
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfyデスクトップ",
|
||||
"ContextMenu": "コンテキストメニュー",
|
||||
"Credits": "クレジット",
|
||||
"CustomColorPalettes": "カスタムカラーパレット",
|
||||
"DevMode": "開発モード",
|
||||
"EditTokenWeight": "トークンの重みを編集",
|
||||
@@ -962,6 +955,7 @@
|
||||
"Sidebar": "サイドバー",
|
||||
"Tree Explorer": "ツリーエクスプローラー",
|
||||
"UV": "UV",
|
||||
"User": "ユーザー",
|
||||
"Validation": "検証",
|
||||
"Window": "ウィンドウ",
|
||||
"Workflow": "ワークフロー"
|
||||
@@ -1116,12 +1110,18 @@
|
||||
"errorCopyImage": "画像のコピーにエラーが発生しました: {error}",
|
||||
"errorLoadingModel": "モデルの読み込みエラー",
|
||||
"errorSaveSetting": "設定{id}の保存エラー: {err}",
|
||||
"failedToAccessBillingPortal": "請求ポータルへのアクセスに失敗しました: {error}",
|
||||
"failedToApplyTexture": "テクスチャの適用に失敗しました",
|
||||
"failedToCreateCustomer": "顧客の作成に失敗しました: {error}",
|
||||
"failedToDownloadFile": "ファイルのダウンロードに失敗しました",
|
||||
"failedToExportModel": "{format}としてモデルのエクスポートに失敗しました",
|
||||
"failedToFetchBalance": "残高の取得に失敗しました: {error}",
|
||||
"failedToFetchLogs": "サーバーログの取得に失敗しました",
|
||||
"failedToInitiateCreditPurchase": "クレジット購入の開始に失敗しました: {error}",
|
||||
"failedToPurchaseCredits": "クレジットの購入に失敗しました: {error}",
|
||||
"fileLoadError": "{fileName}でワークフローが見つかりません",
|
||||
"fileUploadFailed": "ファイルのアップロードに失敗しました",
|
||||
"firebaseAuthNotInitialized": "Firebase認証が初期化されていません",
|
||||
"interrupted": "実行が中断されました",
|
||||
"migrateToLitegraphReroute": "将来のバージョンではRerouteノードが削除されます。litegraph-native rerouteに移行するにはクリックしてください。",
|
||||
"no3dScene": "テクスチャを適用する3Dシーンがありません",
|
||||
@@ -1132,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "保留中のタスクが削除されました",
|
||||
"pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください",
|
||||
"unableToGetModelFilePath": "モデルファイルのパスを取得できません",
|
||||
"updateRequested": "更新が要求されました"
|
||||
"updateRequested": "更新が要求されました",
|
||||
"userNotAuthenticated": "ユーザーが認証されていません"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "ユーザー名を入力してください",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "小"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "サイドバーの幅を統一"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "グリッドサイズにスナップ",
|
||||
"tooltip": "シフトを押しながらノードをドラッグおよびサイズ変更すると、グリッドに整列されます。これにより、そのグリッドのサイズが制御されます。"
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "ズームイン時にノードコンボウィジェットメニュー(リスト)をスケーリングする"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "新しいノードを常に縮小",
|
||||
"tooltip": "ノード作成時に可能な限り小さいサイズにリサイズします。無効にすると、新しく追加されたノードはウィジェットの値が表示されるように少し幅広くなります。"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "ツールチップ遅延"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "로그인 대화상자 열기"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "로그아웃"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "현재 워크플로우 닫기"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"emailPlaceholder": "이메일을 입력하세요",
|
||||
"failed": "로그인 실패",
|
||||
"forgotPassword": "비밀번호를 잊으셨나요?",
|
||||
"genericErrorMessage": "죄송합니다. 오류가 발생했습니다. {supportEmail}로 문의해 주세요.",
|
||||
"forgotPasswordError": "비밀번호 재설정 이메일 전송에 실패했습니다",
|
||||
"loginButton": "로그인",
|
||||
"loginWithGithub": "Github로 로그인",
|
||||
"loginWithGoogle": "구글로 로그인",
|
||||
@@ -25,6 +25,8 @@
|
||||
"orContinueWith": "또는 다음으로 계속",
|
||||
"passwordLabel": "비밀번호",
|
||||
"passwordPlaceholder": "비밀번호를 입력하세요",
|
||||
"passwordResetSent": "비밀번호 재설정 이메일이 전송되었습니다",
|
||||
"passwordResetSentDetail": "비밀번호를 재설정할 수 있는 링크가 포함된 이메일을 확인해 주세요.",
|
||||
"privacyLink": "개인정보 보호정책",
|
||||
"signInOrSignUp": "로그인 / 회원가입",
|
||||
"signUp": "가입하기",
|
||||
@@ -33,20 +35,6 @@
|
||||
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
|
||||
"title": "계정에 로그인"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "설정에서 크레딧 추가 열기",
|
||||
"hint": "크레딧을 추가하려면: 설정 > 사용자 > 크레딧 으로 이동하세요.",
|
||||
"message": "이 워크플로에는 크레딧이 필요한 노드가 포함되어 있습니다. 계속하려면 계정에 크레딧을 추가하세요.",
|
||||
"title": "워크플로 실행을 위한 크레딧 필요"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "설정에서 로그인 열기",
|
||||
"hint": "로그인하려면: 설정 > 사용자 > 로그인 으로 이동하세요.",
|
||||
"message": "이 워크플로에는 활성 계정이 필요한 노드가 포함되어 있습니다. 계속하려면 로그인하거나 계정을 생성하세요.",
|
||||
"title": "워크플로 실행을 위한 로그인 필요"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "로그아웃",
|
||||
"success": "성공적으로 로그아웃되었습니다",
|
||||
@@ -115,18 +103,19 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "크레딧",
|
||||
"creditsHistory": "크레딧 내역",
|
||||
"faqs": "자주 묻는 질문",
|
||||
"invoiceHistory": "청구서 내역",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"messageSupport": "지원 문의",
|
||||
"purchaseCredits": "크레딧 구매",
|
||||
"topUp": {
|
||||
"addCredits": "잔액에 크레딧 추가",
|
||||
"buyNow": "지금 구매",
|
||||
"insufficientMessage": "이 워크플로우를 실행하기에 크레딧이 부족합니다.",
|
||||
"insufficientTitle": "크레딧 부족",
|
||||
"maxAmount": "(최대 $1,000 USD)",
|
||||
"title": "크레딧 잔액 충전"
|
||||
"quickPurchase": "빠른 구매",
|
||||
"seeDetails": "자세히 보기",
|
||||
"topUp": "충전하기"
|
||||
},
|
||||
"yourCreditBalance": "보유 크레딧 잔액"
|
||||
},
|
||||
@@ -308,11 +297,13 @@
|
||||
"success": "성공",
|
||||
"systemInfo": "시스템 정보",
|
||||
"terminal": "터미널",
|
||||
"unknownError": "알 수 없는 오류",
|
||||
"update": "업데이트",
|
||||
"updateAvailable": "업데이트 가능",
|
||||
"updated": "업데이트 됨",
|
||||
"updating": "업데이트 중",
|
||||
"upload": "업로드",
|
||||
"user": "사용자",
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
|
||||
"workflow": "워크플로"
|
||||
},
|
||||
@@ -675,6 +666,7 @@
|
||||
"Save": "저장",
|
||||
"Save As": "다른 이름으로 저장",
|
||||
"Show Settings Dialog": "설정 대화상자 표시",
|
||||
"Sign Out": "로그아웃",
|
||||
"Toggle Bottom Panel": "하단 패널 전환",
|
||||
"Toggle Focus Mode": "포커스 모드 전환",
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
@@ -928,6 +920,7 @@
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfy-Desktop",
|
||||
"ContextMenu": "컨텍스트 메뉴",
|
||||
"Credits": "크레딧",
|
||||
"CustomColorPalettes": "사용자 정의 색상 팔레트",
|
||||
"DevMode": "개발자 모드",
|
||||
"EditTokenWeight": "토큰 가중치 편집",
|
||||
@@ -962,6 +955,7 @@
|
||||
"Sidebar": "사이드바",
|
||||
"Tree Explorer": "트리 탐색기",
|
||||
"UV": "UV",
|
||||
"User": "사용자",
|
||||
"Validation": "검증",
|
||||
"Window": "창",
|
||||
"Workflow": "워크플로"
|
||||
@@ -1116,12 +1110,18 @@
|
||||
"errorCopyImage": "이미지 복사 오류: {error}",
|
||||
"errorLoadingModel": "모델 로딩 오류",
|
||||
"errorSaveSetting": "설정 {id} 저장 오류: {err}",
|
||||
"failedToAccessBillingPortal": "결제 포털에 접근하지 못했습니다: {error}",
|
||||
"failedToApplyTexture": "텍스처 적용에 실패했습니다",
|
||||
"failedToCreateCustomer": "고객 생성에 실패했습니다: {error}",
|
||||
"failedToDownloadFile": "파일 다운로드에 실패했습니다",
|
||||
"failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다",
|
||||
"failedToFetchBalance": "잔액을 가져오지 못했습니다: {error}",
|
||||
"failedToFetchLogs": "서버 로그를 가져오는 데 실패했습니다",
|
||||
"failedToInitiateCreditPurchase": "크레딧 구매를 시작하지 못했습니다: {error}",
|
||||
"failedToPurchaseCredits": "크레딧 구매에 실패했습니다: {error}",
|
||||
"fileLoadError": "{fileName}에서 워크플로우를 찾을 수 없습니다",
|
||||
"fileUploadFailed": "파일 업로드에 실패했습니다",
|
||||
"firebaseAuthNotInitialized": "Firebase 인증이 초기화되지 않았습니다",
|
||||
"interrupted": "실행이 중단되었습니다",
|
||||
"migrateToLitegraphReroute": "향후 버전에서는 Reroute 노드가 제거됩니다. LiteGraph 에서 자체 제공하는 경유점으로 변환하려면 클릭하세요.",
|
||||
"no3dScene": "텍스처를 적용할 3D 장면이 없습니다",
|
||||
@@ -1132,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",
|
||||
"pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요",
|
||||
"unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다",
|
||||
"updateRequested": "업데이트 요청됨"
|
||||
"updateRequested": "업데이트 요청됨",
|
||||
"userNotAuthenticated": "사용자가 인증되지 않았습니다"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "사용자 이름 입력",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "작음"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "통합 사이드바 너비"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "그리드 크기에 스냅",
|
||||
"tooltip": "Shift 키를 누른 상태에서 노드를 드래그하거나 크기를 조절하면 그리드에 맞춰 정렬됩니다. 이 설정은 그리드의 크기를 제어합니다."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "확대시 노드 콤보 위젯 메뉴 (목록) 스케일링"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "새 노드를 항상 축소",
|
||||
"tooltip": "노드를 생성할 때 가능한 가장 작은 크기로 크기를 조정합니다. 비활성화하면 새로 추가된 노드가 위젯 값을 표시하기 위해 약간 넓어집니다."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "툴팁 지연"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Открыть окно входа"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Выйти"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Закрыть текущий рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"emailPlaceholder": "Введите вашу электронную почту",
|
||||
"failed": "Вход не удался",
|
||||
"forgotPassword": "Забыли пароль?",
|
||||
"genericErrorMessage": "Извините, произошла ошибка. Пожалуйста, свяжитесь с {supportEmail}.",
|
||||
"forgotPasswordError": "Не удалось отправить письмо для сброса пароля",
|
||||
"loginButton": "Войти",
|
||||
"loginWithGithub": "Войти через Github",
|
||||
"loginWithGoogle": "Войти через Google",
|
||||
@@ -25,6 +25,8 @@
|
||||
"orContinueWith": "Или продолжить с",
|
||||
"passwordLabel": "Пароль",
|
||||
"passwordPlaceholder": "Введите ваш пароль",
|
||||
"passwordResetSent": "Письмо для сброса пароля отправлено",
|
||||
"passwordResetSentDetail": "Пожалуйста, проверьте свою электронную почту для получения ссылки на сброс пароля.",
|
||||
"privacyLink": "Политикой конфиденциальности",
|
||||
"signInOrSignUp": "Войти / Зарегистрироваться",
|
||||
"signUp": "Зарегистрироваться",
|
||||
@@ -33,20 +35,6 @@
|
||||
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
|
||||
"title": "Войдите в свой аккаунт"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "Открыть настройки для пополнения кредитов",
|
||||
"hint": "Чтобы пополнить кредиты, перейдите в: Настройки > Пользователь > Кредиты",
|
||||
"message": "Этот рабочий процесс содержит узлы, для которых необходимы кредиты. Пожалуйста, пополните баланс, чтобы продолжить.",
|
||||
"title": "Требуются кредиты для выполнения рабочего процесса"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "Открыть настройки для входа",
|
||||
"hint": "Чтобы войти, перейдите в: Настройки > Пользователь > Вход",
|
||||
"message": "Этот рабочий процесс содержит узлы, для которых необходима активная учетная запись. Пожалуйста, войдите или создайте учетную запись, чтобы продолжить.",
|
||||
"title": "Требуется вход для выполнения рабочего процесса"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Выйти",
|
||||
"success": "Вы успешно вышли из системы",
|
||||
@@ -115,18 +103,19 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Кредиты",
|
||||
"creditsHistory": "История кредитов",
|
||||
"faqs": "Часто задаваемые вопросы",
|
||||
"invoiceHistory": "История счетов",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"messageSupport": "Связаться с поддержкой",
|
||||
"purchaseCredits": "Купить кредиты",
|
||||
"topUp": {
|
||||
"addCredits": "Добавить кредиты на баланс",
|
||||
"buyNow": "Купить сейчас",
|
||||
"insufficientMessage": "У вас недостаточно кредитов для запуска этого рабочего процесса.",
|
||||
"insufficientTitle": "Недостаточно кредитов",
|
||||
"maxAmount": "(Макс. $1,000 USD)",
|
||||
"title": "Пополнить баланс кредитов"
|
||||
"quickPurchase": "Быстрая покупка",
|
||||
"seeDetails": "Смотреть детали",
|
||||
"topUp": "Пополнить"
|
||||
},
|
||||
"yourCreditBalance": "Ваш баланс кредитов"
|
||||
},
|
||||
@@ -308,11 +297,13 @@
|
||||
"success": "Успех",
|
||||
"systemInfo": "Информация о системе",
|
||||
"terminal": "Терминал",
|
||||
"unknownError": "Неизвестная ошибка",
|
||||
"update": "Обновить",
|
||||
"updateAvailable": "Доступно обновление",
|
||||
"updated": "Обновлено",
|
||||
"updating": "Обновление",
|
||||
"upload": "Загрузить",
|
||||
"user": "Пользователь",
|
||||
"videoFailedToLoad": "Не удалось загрузить видео",
|
||||
"workflow": "Рабочий процесс"
|
||||
},
|
||||
@@ -675,6 +666,7 @@
|
||||
"Save": "Сохранить",
|
||||
"Save As": "Сохранить как",
|
||||
"Show Settings Dialog": "Показать диалог настроек",
|
||||
"Sign Out": "Выйти",
|
||||
"Toggle Bottom Panel": "Переключить нижнюю панель",
|
||||
"Toggle Focus Mode": "Переключить режим фокуса",
|
||||
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
|
||||
@@ -928,6 +920,7 @@
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Десктопный Comfy",
|
||||
"ContextMenu": "Контекстное меню",
|
||||
"Credits": "Кредиты",
|
||||
"CustomColorPalettes": "Пользовательские цветовые палитры",
|
||||
"DevMode": "Режим разработчика",
|
||||
"EditTokenWeight": "Редактировать вес токена",
|
||||
@@ -962,6 +955,7 @@
|
||||
"Sidebar": "Боковая панель",
|
||||
"Tree Explorer": "Дерево проводника",
|
||||
"UV": "UV",
|
||||
"User": "Пользователь",
|
||||
"Validation": "Валидация",
|
||||
"Window": "Окно",
|
||||
"Workflow": "Рабочий процесс"
|
||||
@@ -1116,12 +1110,18 @@
|
||||
"errorCopyImage": "Ошибка копирования изображения: {error}",
|
||||
"errorLoadingModel": "Ошибка загрузки модели",
|
||||
"errorSaveSetting": "Ошибка сохранения настройки {id}: {err}",
|
||||
"failedToAccessBillingPortal": "Не удалось получить доступ к биллинговому порталу: {error}",
|
||||
"failedToApplyTexture": "Не удалось применить текстуру",
|
||||
"failedToCreateCustomer": "Не удалось создать клиента: {error}",
|
||||
"failedToDownloadFile": "Не удалось скачать файл",
|
||||
"failedToExportModel": "Не удалось экспортировать модель как {format}",
|
||||
"failedToFetchBalance": "Не удалось получить баланс: {error}",
|
||||
"failedToFetchLogs": "Не удалось получить серверные логи",
|
||||
"failedToInitiateCreditPurchase": "Не удалось начать покупку кредитов: {error}",
|
||||
"failedToPurchaseCredits": "Не удалось купить кредиты: {error}",
|
||||
"fileLoadError": "Не удалось найти рабочий процесс в {fileName}",
|
||||
"fileUploadFailed": "Не удалось загрузить файл",
|
||||
"firebaseAuthNotInitialized": "Firebase Auth не инициализирован",
|
||||
"interrupted": "Выполнение было прервано",
|
||||
"migrateToLitegraphReroute": "Узлы перенаправления будут удалены в будущих версиях. Нажмите, чтобы перейти на litegraph-native reroute.",
|
||||
"no3dScene": "Нет 3D сцены для применения текстуры",
|
||||
@@ -1132,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "Ожидающие задачи удалены",
|
||||
"pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы",
|
||||
"unableToGetModelFilePath": "Не удалось получить путь к файлу модели",
|
||||
"updateRequested": "Запрошено обновление"
|
||||
"updateRequested": "Запрошено обновление",
|
||||
"userNotAuthenticated": "Пользователь не аутентифицирован"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "Введите имя пользователя",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "маленький"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Унифицированная ширина боковой панели"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Размер сетки привязки",
|
||||
"tooltip": "При перетаскивании и изменении размера нод, удерживая shift, они будут выровнены по сетке, это контролирует размер этой сетки."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Масштабирование комбинированных виджетов меню узлов (списков) при увеличении"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Всегда сжимать новые узлы",
|
||||
"tooltip": "Изменять размер узлов до минимально возможного при создании. Если отключено, новый узел будет немного расширен для отображения значений виджетов."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Задержка всплывающей подсказки"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "打开登录对话框"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "退出登录"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "关闭当前工作流"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"failed": "登录失败",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"genericErrorMessage": "抱歉,我们遇到了一些错误。请联系 {supportEmail}。",
|
||||
"forgotPasswordError": "发送重置密码邮件失败",
|
||||
"loginButton": "登录",
|
||||
"loginWithGithub": "使用Github登录",
|
||||
"loginWithGoogle": "使用Google登录",
|
||||
@@ -25,6 +25,8 @@
|
||||
"orContinueWith": "或者继续使用",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入您的密码",
|
||||
"passwordResetSent": "重置密码邮件已发送",
|
||||
"passwordResetSentDetail": "请查收您的电子邮件,点击链接重置密码。",
|
||||
"privacyLink": "隐私政策",
|
||||
"signInOrSignUp": "登录 / 注册",
|
||||
"signUp": "注册",
|
||||
@@ -33,20 +35,6 @@
|
||||
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
|
||||
"title": "登录您的账户"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "打开设置进行充值",
|
||||
"hint": "要充值,请前往:设置 > 用户 > 积分",
|
||||
"message": "此工作流包含需要积分的节点。请为您的账户充值以继续。",
|
||||
"title": "需要积分以执行工作流"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "打开设置进行登录",
|
||||
"hint": "要登录,请前往:设置 > 用户 > 登录",
|
||||
"message": "此工作流包含需要活跃账户的节点。请登录或创建账户以继续。",
|
||||
"title": "需要登录以执行工作流"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "退出登录",
|
||||
"success": "成功退出登录",
|
||||
@@ -115,18 +103,19 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "积分",
|
||||
"creditsHistory": "积分历史",
|
||||
"faqs": "常见问题",
|
||||
"invoiceHistory": "发票历史",
|
||||
"lastUpdated": "最近更新",
|
||||
"messageSupport": "联系客服",
|
||||
"purchaseCredits": "购买积分",
|
||||
"topUp": {
|
||||
"addCredits": "为您的余额充值",
|
||||
"buyNow": "立即购买",
|
||||
"insufficientMessage": "您的积分不足,无法运行此工作流。",
|
||||
"insufficientTitle": "积分不足",
|
||||
"maxAmount": "(最高 $1,000 美元)",
|
||||
"title": "充值余额"
|
||||
"quickPurchase": "快速购买",
|
||||
"seeDetails": "查看详情",
|
||||
"topUp": "充值"
|
||||
},
|
||||
"yourCreditBalance": "您的积分余额"
|
||||
},
|
||||
@@ -308,11 +297,13 @@
|
||||
"success": "成功",
|
||||
"systemInfo": "系统信息",
|
||||
"terminal": "终端",
|
||||
"unknownError": "未知错误",
|
||||
"update": "更新",
|
||||
"updateAvailable": "有更新可用",
|
||||
"updated": "已更新",
|
||||
"updating": "更新中",
|
||||
"upload": "上传",
|
||||
"user": "用户",
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
@@ -675,6 +666,7 @@
|
||||
"Save": "保存",
|
||||
"Save As": "另存为",
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
@@ -928,6 +920,7 @@
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfy桌面版",
|
||||
"ContextMenu": "上下文菜单",
|
||||
"Credits": "积分",
|
||||
"CustomColorPalettes": "自定义色彩主题",
|
||||
"DevMode": "开发模式",
|
||||
"EditTokenWeight": "编辑令牌权重",
|
||||
@@ -962,6 +955,7 @@
|
||||
"Sidebar": "侧边栏",
|
||||
"Tree Explorer": "树形浏览器",
|
||||
"UV": "UV",
|
||||
"User": "用户",
|
||||
"Validation": "验证",
|
||||
"Window": "窗口",
|
||||
"Workflow": "工作流"
|
||||
@@ -1116,12 +1110,18 @@
|
||||
"errorCopyImage": "复制图片出错:{error}",
|
||||
"errorLoadingModel": "加载模型出错",
|
||||
"errorSaveSetting": "保存设置 {id} 出错:{err}",
|
||||
"failedToAccessBillingPortal": "访问账单门户失败:{error}",
|
||||
"failedToApplyTexture": "应用纹理失败",
|
||||
"failedToCreateCustomer": "创建客户失败:{error}",
|
||||
"failedToDownloadFile": "文件下载失败",
|
||||
"failedToExportModel": "无法将模型导出为 {format}",
|
||||
"failedToFetchBalance": "获取余额失败:{error}",
|
||||
"failedToFetchLogs": "无法获取服务器日志",
|
||||
"failedToInitiateCreditPurchase": "发起积分购买失败:{error}",
|
||||
"failedToPurchaseCredits": "购买积分失败:{error}",
|
||||
"fileLoadError": "无法在 {fileName} 中找到工作流",
|
||||
"fileUploadFailed": "文件上传失败",
|
||||
"firebaseAuthNotInitialized": "Firebase 认证未初始化",
|
||||
"interrupted": "执行已被中断",
|
||||
"migrateToLitegraphReroute": "将来的版本中将删除重定向节点。点击以迁移到litegraph-native重定向。",
|
||||
"no3dScene": "没有3D场景可以应用纹理",
|
||||
@@ -1132,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "待处理任务已删除",
|
||||
"pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组",
|
||||
"unableToGetModelFilePath": "无法获取模型文件路径",
|
||||
"updateRequested": "已请求更新"
|
||||
"updateRequested": "已请求更新",
|
||||
"userNotAuthenticated": "用户未认证"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "输入用户名",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "小"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "统一侧边栏宽度"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "吸附网格大小",
|
||||
"tooltip": "在按住shift拖动和调整节点大小时,节点将对齐到网格,该选项控制网格。"
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "放大时缩放节点组合部件菜单(列表)"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "始终收缩新节点",
|
||||
"tooltip": "创建节点时将其缩小到最小可能尺寸。禁用后,新添加的节点会略微加宽以显示控件数值。"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "工具提示延迟"
|
||||
},
|
||||
|
||||
@@ -384,6 +384,7 @@ const zSettings = z.object({
|
||||
'Comfy.PromptFilename': z.boolean(),
|
||||
'Comfy.Sidebar.Location': z.enum(['left', 'right']),
|
||||
'Comfy.Sidebar.Size': z.enum(['small', 'normal']),
|
||||
'Comfy.Sidebar.UnifiedWidth': z.boolean(),
|
||||
'Comfy.SwitchUser': z.any(),
|
||||
'Comfy.SnapToGrid.GridSize': z.number(),
|
||||
'Comfy.TextareaWidget.FontSize': z.number(),
|
||||
@@ -454,7 +455,8 @@ const zSettings = z.object({
|
||||
/** Settings used for testing */
|
||||
'test.setting': z.any(),
|
||||
'main.sub.setting.name': z.any(),
|
||||
'single.setting': z.any()
|
||||
'single.setting': z.any(),
|
||||
'LiteGraph.Node.DefaultPadding': z.boolean()
|
||||
})
|
||||
|
||||
export type EmbeddingsResponse = z.infer<typeof zEmbeddingsResponse>
|
||||
|
||||
@@ -77,7 +77,8 @@ export const zComfyNodeDef = z.object({
|
||||
output_node: z.boolean(),
|
||||
python_module: z.string(),
|
||||
deprecated: z.boolean().optional(),
|
||||
experimental: z.boolean().optional()
|
||||
experimental: z.boolean().optional(),
|
||||
api_node: z.boolean().optional()
|
||||
})
|
||||
|
||||
// Export types
|
||||
|
||||
@@ -25,6 +25,7 @@ export const zBaseInputOptions = z
|
||||
tooltip: z.string().optional(),
|
||||
hidden: z.boolean().optional(),
|
||||
advanced: z.boolean().optional(),
|
||||
widgetType: z.string().optional(),
|
||||
/** Backend-only properties. */
|
||||
rawLink: z.boolean().optional(),
|
||||
lazy: z.boolean().optional()
|
||||
@@ -222,7 +223,13 @@ export const zComfyNodeDef = z.object({
|
||||
output_node: z.boolean(),
|
||||
python_module: z.string(),
|
||||
deprecated: z.boolean().optional(),
|
||||
experimental: z.boolean().optional()
|
||||
experimental: z.boolean().optional(),
|
||||
/**
|
||||
* Whether the node is an API node. Running API nodes requires login to
|
||||
* Comfy Org account.
|
||||
* https://www.comfy.org/faq
|
||||
*/
|
||||
api_node: z.boolean().optional()
|
||||
})
|
||||
|
||||
// `/object_info`
|
||||
|
||||
@@ -215,6 +215,20 @@ export class ComfyApi extends EventTarget {
|
||||
|
||||
reportedUnknownMessageTypes = new Set<string>()
|
||||
|
||||
/**
|
||||
* The auth token for the comfy org account if the user is logged in.
|
||||
* This is only used for {@link queuePrompt} now. It is not directly
|
||||
* passed as parameter to the function because some custom nodes are hijacking
|
||||
* {@link queuePrompt} improperly, which causes extra parameters to be lost
|
||||
* in the function call chain.
|
||||
*
|
||||
* Ref: https://cs.comfy.org/search?q=context:global+%22api.queuePrompt+%3D%22&patternType=keyword&sm=0
|
||||
*
|
||||
* TODO: Move this field to parameter of {@link queuePrompt} once all
|
||||
* custom nodes are patched.
|
||||
*/
|
||||
authToken?: string
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.user = ''
|
||||
@@ -517,13 +531,11 @@ export class ComfyApi extends EventTarget {
|
||||
* Queues a prompt to be executed
|
||||
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
|
||||
* @param {object} prompt The prompt data to queue
|
||||
* @param {string} authToken The auth token for the comfy org account if the user is logged in
|
||||
* @throws {PromptExecutionError} If the prompt fails to execute
|
||||
*/
|
||||
async queuePrompt(
|
||||
number: number,
|
||||
data: { output: ComfyApiWorkflow; workflow: ComfyWorkflowJSON },
|
||||
authToken?: string
|
||||
data: { output: ComfyApiWorkflow; workflow: ComfyWorkflowJSON }
|
||||
): Promise<PromptResponse> {
|
||||
const { output: prompt, workflow } = data
|
||||
|
||||
@@ -531,7 +543,7 @@ export class ComfyApi extends EventTarget {
|
||||
client_id: this.clientId ?? '', // TODO: Unify clientId access
|
||||
prompt,
|
||||
extra_data: {
|
||||
auth_token_comfy_org: authToken,
|
||||
auth_token_comfy_org: this.authToken,
|
||||
extra_pnginfo: { workflow }
|
||||
}
|
||||
}
|
||||
@@ -870,52 +882,6 @@ export class ComfyApi extends EventTarget {
|
||||
return resp
|
||||
}
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* Lists user data files for the current user
|
||||
* @param { string } dir The directory in which to list files
|
||||
* @param { boolean } [recurse] If the listing should be recursive
|
||||
* @param { true } [split] If the paths should be split based on the os path separator
|
||||
* @returns { Promise<string[][]> } The list of split file paths in the format [fullPath, ...splitPath]
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* Lists user data files for the current user
|
||||
* @param { string } dir The directory in which to list files
|
||||
* @param { boolean } [recurse] If the listing should be recursive
|
||||
* @param { false | undefined } [split] If the paths should be split based on the os path separator
|
||||
* @returns { Promise<string[]> } The list of files
|
||||
*/
|
||||
async listUserData(
|
||||
dir: string,
|
||||
recurse: boolean,
|
||||
split?: true
|
||||
): Promise<string[][]>
|
||||
async listUserData(
|
||||
dir: string,
|
||||
recurse: boolean,
|
||||
split?: false
|
||||
): Promise<string[]>
|
||||
/**
|
||||
* @deprecated Use `listUserDataFullInfo` instead.
|
||||
*/
|
||||
async listUserData(dir: string, recurse: boolean, split?: boolean) {
|
||||
const resp = await this.fetchApi(
|
||||
`/userdata?${new URLSearchParams({
|
||||
recurse: recurse ? 'true' : 'false',
|
||||
dir,
|
||||
split: split ? 'true' : 'false'
|
||||
})}`
|
||||
)
|
||||
if (resp.status === 404) return []
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(
|
||||
`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`
|
||||
)
|
||||
}
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
async listUserDataFullInfo(dir: string): Promise<UserDataFullInfo[]> {
|
||||
const resp = await this.fetchApi(
|
||||
`/userdata?dir=${encodeURIComponent(dir)}&recurse=true&split=false&full_info=true`
|
||||
|
||||
@@ -674,13 +674,18 @@ export class ComfyApp {
|
||||
|
||||
api.addEventListener('execution_error', ({ detail }) => {
|
||||
// Check if this is an auth-related error or credits-related error
|
||||
if (detail.exception_message === 'Please login first to use this node.') {
|
||||
useDialogService().showSignInRequiredDialog({ type: 'signIn' })
|
||||
if (
|
||||
detail.exception_message ===
|
||||
'Unauthorized: Please login first to use this node.'
|
||||
) {
|
||||
useDialogService().showApiNodesSignInDialog([detail.node_type])
|
||||
} else if (
|
||||
detail.exception_message ===
|
||||
'Payment Required: Please add credits to your account to use this node.'
|
||||
) {
|
||||
useDialogService().showSignInRequiredDialog({ type: 'credits' })
|
||||
useDialogService().showTopUpCreditsDialog({
|
||||
isInsufficientCredits: true
|
||||
})
|
||||
} else {
|
||||
useDialogService().showExecutionErrorDialog(detail)
|
||||
}
|
||||
@@ -1203,7 +1208,9 @@ export class ComfyApp {
|
||||
|
||||
const p = await this.graphToPrompt()
|
||||
try {
|
||||
const res = await api.queuePrompt(number, p, comfyOrgAuthToken)
|
||||
api.authToken = comfyOrgAuthToken
|
||||
const res = await api.queuePrompt(number, p)
|
||||
delete api.authToken
|
||||
executionStore.lastNodeErrors = res.node_errors ?? null
|
||||
if (executionStore.lastNodeErrors?.length) {
|
||||
this.canvas.draw(true, true)
|
||||
|
||||
@@ -8,7 +8,6 @@ import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarni
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SignInContent from '@/components/dialog/content/SignInContent.vue'
|
||||
import SignInRequiredDialogContent from '@/components/dialog/content/SignInRequiredDialogContent.vue'
|
||||
import TopUpCreditsDialogContent from '@/components/dialog/content/TopUpCreditsDialogContent.vue'
|
||||
import ManagerDialogContent from '@/components/dialog/content/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/components/dialog/content/manager/ManagerHeader.vue'
|
||||
@@ -348,20 +347,13 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showSignInRequiredDialog(options: { type: 'signIn' | 'credits' }) {
|
||||
dialogStore.showDialog({
|
||||
key: 'signin-required',
|
||||
component: SignInRequiredDialogContent,
|
||||
props: options
|
||||
})
|
||||
}
|
||||
|
||||
function showTopUpCreditsDialog(options?: {
|
||||
isInsufficientCredits?: boolean
|
||||
}) {
|
||||
return dialogStore.showDialog({
|
||||
key: 'top-up-credits',
|
||||
component: TopUpCreditsDialogContent,
|
||||
headerComponent: ComfyOrgHeader,
|
||||
props: options,
|
||||
dialogComponentProps: {
|
||||
pt: {
|
||||
@@ -384,7 +376,6 @@ export const useDialogService = () => {
|
||||
showErrorDialog,
|
||||
showApiNodesSignInDialog,
|
||||
showSignInDialog,
|
||||
showSignInRequiredDialog,
|
||||
showTopUpCreditsDialog,
|
||||
prompt,
|
||||
confirm
|
||||
|
||||
90
src/services/firebaseAuthService.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Service for Firebase Auth actions.
|
||||
* All actions are wrapped with error handling.
|
||||
* @returns {Object} - Object containing all Firebase Auth actions
|
||||
*/
|
||||
export const useFirebaseAuthService = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const toastStore = useToastStore()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const reportError = (error: unknown) => {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
|
||||
const logout = wrapWithErrorHandlingAsync(async () => {
|
||||
await authStore.logout()
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.signOut.success'),
|
||||
detail: t('auth.signOut.successDetail'),
|
||||
life: 5000
|
||||
})
|
||||
}, reportError)
|
||||
|
||||
const sendPasswordReset = wrapWithErrorHandlingAsync(
|
||||
async (email: string) => {
|
||||
await authStore.sendPasswordReset(email)
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.login.passwordResetSent'),
|
||||
detail: t('auth.login.passwordResetSentDetail'),
|
||||
life: 5000
|
||||
})
|
||||
},
|
||||
reportError
|
||||
)
|
||||
|
||||
const purchaseCredits = wrapWithErrorHandlingAsync(async (amount: number) => {
|
||||
const response = await authStore.initiateCreditPurchase({
|
||||
amount_micros: usdToMicros(amount),
|
||||
currency: 'usd'
|
||||
})
|
||||
|
||||
if (!response.checkout_url) {
|
||||
throw new Error(
|
||||
t('toastMessages.failedToPurchaseCredits', {
|
||||
error: 'No checkout URL returned'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Go to Stripe checkout page
|
||||
window.open(response.checkout_url, '_blank')
|
||||
}, reportError)
|
||||
|
||||
const accessBillingPortal = wrapWithErrorHandlingAsync(async () => {
|
||||
const response = await authStore.accessBillingPortal()
|
||||
if (!response.billing_portal_url) {
|
||||
throw new Error(
|
||||
t('toastMessages.failedToAccessBillingPortal', {
|
||||
error: 'No billing portal URL returned'
|
||||
})
|
||||
)
|
||||
}
|
||||
window.open(response.billing_portal_url, '_blank')
|
||||
}, reportError)
|
||||
|
||||
const fetchBalance = wrapWithErrorHandlingAsync(async () => {
|
||||
await authStore.fetchBalance()
|
||||
}, reportError)
|
||||
|
||||
return {
|
||||
logout,
|
||||
sendPasswordReset,
|
||||
purchaseCredits,
|
||||
accessBillingPortal,
|
||||
fetchBalance
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { $el } from '@/scripts/ui'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
@@ -113,7 +114,9 @@ export const useLitegraphService = () => {
|
||||
#addInputSocket(inputSpec: InputSpec) {
|
||||
const inputName = inputSpec.name
|
||||
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
|
||||
const widgetConstructor = widgetStore.widgets.get(inputSpec.type)
|
||||
const widgetConstructor = widgetStore.widgets.get(
|
||||
inputSpec.widgetType ?? inputSpec.type
|
||||
)
|
||||
if (widgetConstructor && !inputSpec.forceInput) return
|
||||
|
||||
this.addInput(inputName, inputSpec.type, {
|
||||
@@ -127,9 +130,13 @@ export const useLitegraphService = () => {
|
||||
* (unless `socketless`), an input socket is also added.
|
||||
*/
|
||||
#addInputWidget(inputSpec: InputSpec) {
|
||||
const widgetInputSpec = { ...inputSpec }
|
||||
if (inputSpec.widgetType) {
|
||||
widgetInputSpec.type = inputSpec.widgetType
|
||||
}
|
||||
const inputName = inputSpec.name
|
||||
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
|
||||
const widgetConstructor = widgetStore.widgets.get(inputSpec.type)
|
||||
const widgetConstructor = widgetStore.widgets.get(widgetInputSpec.type)
|
||||
if (!widgetConstructor || inputSpec.forceInput) return
|
||||
|
||||
const {
|
||||
@@ -139,7 +146,7 @@ export const useLitegraphService = () => {
|
||||
} = widgetConstructor(
|
||||
this,
|
||||
inputName,
|
||||
transformInputSpecV2ToV1(inputSpec),
|
||||
transformInputSpecV2ToV1(widgetInputSpec),
|
||||
app
|
||||
) ?? {}
|
||||
|
||||
@@ -153,7 +160,7 @@ export const useLitegraphService = () => {
|
||||
}
|
||||
|
||||
if (!widget?.options?.socketless) {
|
||||
const inputSpecV1 = transformInputSpecV2ToV1(inputSpec)
|
||||
const inputSpecV1 = transformInputSpecV2ToV1(widgetInputSpec)
|
||||
this.addInput(inputName, inputSpec.type, {
|
||||
shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined,
|
||||
localized_name: st(nameKey, inputName),
|
||||
@@ -208,7 +215,11 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setInitialSize() {
|
||||
const s = this.computeSize()
|
||||
s[0] = Math.max(this.#initialMinSize.width, s[0] * 1.5)
|
||||
// Expand the width a little to fit widget values on screen.
|
||||
const pad =
|
||||
this.widgets?.length &&
|
||||
!useSettingStore().get('LiteGraph.Node.DefaultPadding')
|
||||
s[0] = Math.max(this.#initialMinSize.width, s[0] + (pad ? 60 : 0))
|
||||
s[1] = Math.max(this.#initialMinSize.height, s[1])
|
||||
this.setSize(s)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
browserLocalPersistence,
|
||||
createUserWithEmailAndPassword,
|
||||
onAuthStateChanged,
|
||||
sendPasswordResetEmail,
|
||||
setPersistence,
|
||||
signInWithEmailAndPassword,
|
||||
signInWithPopup,
|
||||
@@ -18,11 +19,8 @@ import { useFirebaseAuth } from 'vuefire'
|
||||
|
||||
import { COMFY_API_BASE_URL } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
import { useToastStore } from './toastStore'
|
||||
|
||||
type CreditPurchaseResponse =
|
||||
operations['InitiateCreditPurchase']['responses']['201']['content']['application/json']
|
||||
type CreditPurchasePayload =
|
||||
@@ -36,10 +34,16 @@ type AccessBillingPortalResponse =
|
||||
type AccessBillingPortalReqBody =
|
||||
operations['AccessBillingPortal']['requestBody']
|
||||
|
||||
export class FirebaseAuthStoreError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'FirebaseAuthStoreError'
|
||||
}
|
||||
}
|
||||
|
||||
export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const currentUser = ref<User | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
const customerCreated = ref(false)
|
||||
@@ -59,32 +63,21 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const userId = computed(() => currentUser.value?.uid)
|
||||
|
||||
// Get auth from VueFire and listen for auth state changes
|
||||
const auth = useFirebaseAuth()
|
||||
if (auth) {
|
||||
// Set persistence to localStorage (works in both browser and Electron)
|
||||
void setPersistence(auth, browserLocalPersistence)
|
||||
// From useFirebaseAuth docs:
|
||||
// Retrieves the Firebase Auth instance. Returns `null` on the server.
|
||||
// When using this function on the client in TypeScript, you can force the type with `useFirebaseAuth()!`.
|
||||
const auth = useFirebaseAuth()!
|
||||
// Set persistence to localStorage (works in both browser and Electron)
|
||||
void setPersistence(auth, browserLocalPersistence)
|
||||
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
currentUser.value = user
|
||||
isInitialized.value = true
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
currentUser.value = user
|
||||
isInitialized.value = true
|
||||
|
||||
// Reset balance when auth state changes
|
||||
balance.value = null
|
||||
lastBalanceUpdateTime.value = null
|
||||
})
|
||||
} else {
|
||||
error.value = 'Firebase Auth not available from VueFire'
|
||||
}
|
||||
|
||||
const showAuthErrorToast = () => {
|
||||
useToastStore().add({
|
||||
summary: t('g.error'),
|
||||
detail: t('auth.login.genericErrorMessage', {
|
||||
supportEmail: 'support@comfy.org'
|
||||
}),
|
||||
severity: 'error'
|
||||
})
|
||||
}
|
||||
// Reset balance when auth state changes
|
||||
balance.value = null
|
||||
lastBalanceUpdateTime.value = null
|
||||
})
|
||||
|
||||
const getIdToken = async (): Promise<string | null> => {
|
||||
if (currentUser.value) {
|
||||
@@ -98,9 +91,10 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
try {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
error.value = 'Cannot fetch balance: User not authenticated'
|
||||
isFetchingBalance.value = false
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/balance`, {
|
||||
@@ -115,8 +109,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return null
|
||||
}
|
||||
const errorData = await response.json()
|
||||
error.value = `Failed to fetch balance: ${errorData.message}`
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToFetchBalance', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const balanceData = await response.json()
|
||||
@@ -124,9 +121,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
lastBalanceUpdateTime.value = new Date()
|
||||
balance.value = balanceData
|
||||
return balanceData
|
||||
} catch (e) {
|
||||
error.value = `Failed to fetch balance: ${e}`
|
||||
return null
|
||||
} finally {
|
||||
isFetchingBalance.value = false
|
||||
}
|
||||
@@ -143,15 +137,21 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
}
|
||||
})
|
||||
if (!createCustomerRes.ok) {
|
||||
throw new Error(
|
||||
`Failed to create customer: ${createCustomerRes.statusText}`
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: createCustomerRes.statusText
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const createCustomerResJson: CreateCustomerResponse =
|
||||
await createCustomerRes.json()
|
||||
if (!createCustomerResJson?.id) {
|
||||
throw new Error('Failed to create customer: No customer ID returned')
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: 'No customer ID returned'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return createCustomerResJson
|
||||
@@ -163,10 +163,12 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
createCustomer?: boolean
|
||||
} = {}
|
||||
): Promise<T> => {
|
||||
if (!auth) throw new Error('Firebase Auth not initialized')
|
||||
if (!auth)
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.firebaseAuthNotInitialized')
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const result = await action(auth)
|
||||
@@ -181,10 +183,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Unknown error'
|
||||
showAuthErrorToast()
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -226,13 +224,17 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const logout = async (): Promise<void> =>
|
||||
executeAuthAction((authInstance) => signOut(authInstance))
|
||||
|
||||
const sendPasswordReset = async (email: string): Promise<void> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
sendPasswordResetEmail(authInstance, email)
|
||||
)
|
||||
|
||||
const addCredits = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse | null> => {
|
||||
): Promise<CreditPurchaseResponse> => {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
error.value = 'Cannot add credits: User not authenticated'
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
// Ensure customer was created during login/registration
|
||||
@@ -252,9 +254,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
error.value = `Failed to initiate credit purchase: ${errorData.message}`
|
||||
showAuthErrorToast()
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToInitiateCreditPurchase', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
@@ -262,24 +266,15 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
const initiateCreditPurchase = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse | null> =>
|
||||
): Promise<CreditPurchaseResponse> =>
|
||||
executeAuthAction((_) => addCredits(requestBodyContent))
|
||||
|
||||
const openSignInPanel = () => {
|
||||
useDialogService().showSettingsDialog('user')
|
||||
}
|
||||
|
||||
const openCreditsPanel = () => {
|
||||
useDialogService().showSettingsDialog('credits')
|
||||
}
|
||||
|
||||
const accessBillingPortal = async (
|
||||
requestBody?: AccessBillingPortalReqBody
|
||||
): Promise<AccessBillingPortalResponse | null> => {
|
||||
): Promise<AccessBillingPortalResponse> => {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
error.value = 'Cannot access billing portal: User not authenticated'
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/billing`, {
|
||||
@@ -295,9 +290,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
error.value = `Failed to access billing portal: ${errorData.message}`
|
||||
showAuthErrorToast()
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToAccessBillingPortal', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
@@ -306,7 +303,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return {
|
||||
// State
|
||||
loading,
|
||||
error,
|
||||
currentUser,
|
||||
isInitialized,
|
||||
balance,
|
||||
@@ -326,9 +322,8 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
loginWithGoogle,
|
||||
loginWithGithub,
|
||||
initiateCreditPurchase,
|
||||
openSignInPanel,
|
||||
openCreditsPanel,
|
||||
fetchBalance,
|
||||
accessBillingPortal
|
||||
accessBillingPortal,
|
||||
sendPasswordReset
|
||||
}
|
||||
})
|
||||
|
||||
@@ -41,6 +41,7 @@ export class ComfyNodeDefImpl
|
||||
readonly deprecated: boolean
|
||||
readonly experimental: boolean
|
||||
readonly output_node: boolean
|
||||
readonly api_node: boolean
|
||||
/**
|
||||
* @deprecated Use `inputs` instead
|
||||
*/
|
||||
@@ -121,6 +122,7 @@ export class ComfyNodeDefImpl
|
||||
this.experimental =
|
||||
obj.experimental ?? obj.category.startsWith('_for_testing')
|
||||
this.output_node = obj.output_node
|
||||
this.api_node = !!obj.api_node
|
||||
this.input = obj.input ?? {}
|
||||
this.output = obj.output ?? []
|
||||
this.output_is_list = obj.output_is_list
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="comfyui-body grid h-screen w-screen overflow-hidden">
|
||||
<div class="comfyui-body grid h-full w-full overflow-hidden">
|
||||
<div id="comfyui-body-top" class="comfyui-body-top">
|
||||
<TopMenubar v-if="useNewMenu === 'Top'" />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<main class="w-full min-h-screen overflow-hidden relative">
|
||||
<main class="w-full h-full overflow-hidden relative">
|
||||
<router-view />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -518,4 +518,23 @@ describe('ComfyNodeDefImpl', () => {
|
||||
expect(result.inputs['booleanInput']).toBeDefined()
|
||||
expect(result.inputs['floatInput']).toBeDefined()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ api_node: true, expected: true },
|
||||
{ api_node: false, expected: false },
|
||||
{ api_node: undefined, expected: false }
|
||||
] as { api_node: boolean | undefined; expected: boolean }[])(
|
||||
'should handle api_node field: $api_node',
|
||||
({ api_node, expected }) => {
|
||||
const result = new ComfyNodeDefImpl({
|
||||
name: 'ApiNode',
|
||||
display_name: 'API Node',
|
||||
category: 'Test',
|
||||
python_module: 'test_module',
|
||||
description: 'A node with API',
|
||||
api_node
|
||||
} as ComfyNodeDefV1)
|
||||
expect(result.api_node).toBe(expected)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -135,7 +135,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
expect(store.userEmail).toBe('test@example.com')
|
||||
expect(store.userId).toBe('test-user-id')
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should set persistence to local storage on initialization', () => {
|
||||
@@ -158,34 +157,12 @@ describe('useFirebaseAuthStore', () => {
|
||||
// Error expected
|
||||
}
|
||||
|
||||
expect(store.error).toBe('Invalid password')
|
||||
|
||||
// Now, succeed on next attempt
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValueOnce({
|
||||
user: mockUser
|
||||
} as any)
|
||||
|
||||
await store.login('test@example.com', 'correct-password')
|
||||
|
||||
// Error should be cleared
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle auth initialization failure', async () => {
|
||||
// Mock auth as null to simulate initialization failure
|
||||
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(null)
|
||||
|
||||
// Create a new store instance
|
||||
setActivePinia(createPinia())
|
||||
const uninitializedStore = useFirebaseAuthStore()
|
||||
|
||||
// Check that isInitialized is false
|
||||
expect(uninitializedStore.isInitialized).toBe(false)
|
||||
|
||||
// Verify store actions throw appropriate errors
|
||||
await expect(
|
||||
uninitializedStore.login('test@example.com', 'password')
|
||||
).rejects.toThrow('Firebase Auth not initialized')
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
@@ -204,7 +181,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle login errors', async () => {
|
||||
@@ -223,7 +199,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
'wrong-password'
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Invalid password')
|
||||
})
|
||||
|
||||
it('should handle concurrent login attempts correctly', async () => {
|
||||
@@ -260,7 +235,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle registration errors', async () => {
|
||||
@@ -279,7 +253,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
'password'
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Email already in use')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -299,7 +272,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
await expect(store.logout()).rejects.toThrow('Network error')
|
||||
|
||||
expect(firebaseAuth.signOut).toHaveBeenCalledWith(mockAuth)
|
||||
expect(store.error).toBe('Network error')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -373,7 +345,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle Google sign in errors', async () => {
|
||||
@@ -389,7 +360,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
expect.any(firebaseAuth.GoogleAuthProvider)
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Google authentication failed')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -408,7 +378,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle Github sign in errors', async () => {
|
||||
@@ -424,7 +393,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
expect.any(firebaseAuth.GithubAuthProvider)
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Github authentication failed')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ export default defineConfig({
|
||||
comfyAPIPlugin(IS_DEV),
|
||||
generateImportMapPlugin([
|
||||
{ name: 'vue', pattern: /[\\/]node_modules[\\/]vue[\\/]/ },
|
||||
{ name: 'primevue', pattern: /[\\/]node_modules[\\/]primevue[\\/]/ }
|
||||
{ name: 'primevue', pattern: /[\\/]node_modules[\\/]primevue[\\/]/ },
|
||||
{ name: 'vue-i18n', pattern: /[\\/]node_modules[\\/]vue-i18n[\\/]/ }
|
||||
]),
|
||||
addElementVnodeExportPlugin(),
|
||||
|
||||
|
||||