Compare commits
75 Commits
v1.17.4
...
sidebar-ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a9eebad30 | ||
|
|
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 | ||
|
|
a57be36d4d | ||
|
|
3bd508c001 | ||
|
|
612500a4dc | ||
|
|
e9723407d8 | ||
|
|
a01aa39423 | ||
|
|
ab94a55858 | ||
|
|
1bcf5e28d4 | ||
|
|
9e247063aa | ||
|
|
cdddf359a8 | ||
|
|
8558f87547 | ||
|
|
262991db6b | ||
|
|
585d52e24e | ||
|
|
b7535755f0 | ||
|
|
6b7b0f6ec1 | ||
|
|
c7318bcf0a | ||
|
|
11f909436c | ||
|
|
d8f4dc95bb | ||
|
|
c1bc664edd | ||
|
|
e7fe2046ba | ||
|
|
bf4ad38e9b | ||
|
|
2b024bb186 | ||
|
|
6e5930c355 | ||
|
|
6151d487c6 | ||
|
|
e027a9bf44 | ||
|
|
53ee5904e8 | ||
|
|
f82bb71b1e | ||
|
|
40d08a890d | ||
|
|
ebf3c0c049 |
2
.github/workflows/release.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
USE_PROD_FIREBASE_CONFIG: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
modelName: 'o4-mini',
|
||||
modelName: 'gpt-4.1',
|
||||
splitToken: 1024,
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
|
||||
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
|
||||
|
||||
@@ -926,7 +926,7 @@ export class ComfyPage {
|
||||
async getNodeRefById(id: NodeId) {
|
||||
return new NodeReference(id, this)
|
||||
}
|
||||
async getNodes() {
|
||||
async getNodes(): Promise<LGraphNode[]> {
|
||||
return await this.page.evaluate(() => {
|
||||
return window['app'].graph.nodes
|
||||
})
|
||||
|
||||
@@ -341,3 +341,30 @@ test.describe('Error dialog', () => {
|
||||
await expect(errorDialog).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Signin dialog', () => {
|
||||
test('Paste content to signin dialog should not paste node on canvas', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodeNum = (await comfyPage.getNodes()).length
|
||||
await comfyPage.clickEmptyLatentNode()
|
||||
await comfyPage.ctrlC()
|
||||
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.fill('test_password')
|
||||
await textBox.press('Control+a')
|
||||
await textBox.press('Control+c')
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].extensionManager.dialog.showSignInDialog()
|
||||
})
|
||||
|
||||
const input = comfyPage.page.locator('#comfy-org-sign-in-password')
|
||||
await input.waitFor({ state: 'visible' })
|
||||
await input.press('Control+v')
|
||||
await expect(input).toHaveValue('test_password')
|
||||
|
||||
expect(await comfyPage.getNodes()).toHaveLength(nodeNum)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,4 +24,11 @@ test.describe('DOM Widget', () => {
|
||||
await expect(firstMultiline).not.toBeVisible()
|
||||
await expect(lastMultiline).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Position update when entering focus mode', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
@@ -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: 102 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: 103 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 |
2
global.d.ts
vendored
@@ -3,7 +3,7 @@ declare const __SENTRY_ENABLED__: boolean
|
||||
declare const __SENTRY_DSN__: string
|
||||
declare const __ALGOLIA_APP_ID__: string
|
||||
declare const __ALGOLIA_API_KEY__: string
|
||||
declare const __USE_PROD_FIREBASE_CONFIG__: boolean
|
||||
declare const __USE_PROD_CONFIG__: boolean
|
||||
|
||||
interface Navigator {
|
||||
/**
|
||||
|
||||
20
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.17.4",
|
||||
"version": "1.18.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.17.4",
|
||||
"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.6",
|
||||
"@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.6",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.13.6.tgz",
|
||||
"integrity": "sha512-a7FWNYayohJHZplEmvBAwlsm7IRqGvGCGfqS70wKGqihpNiHVldPecSoVr6cL/tDkObSiSj0/5d6ueTCjZXEsA==",
|
||||
"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.4",
|
||||
"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.6",
|
||||
"@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>
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
:label="$t('issueReport.helpFix')"
|
||||
@click="showSendReport"
|
||||
/>
|
||||
<Button
|
||||
v-if="authStore.currentUser"
|
||||
v-show="!reportOpen"
|
||||
text
|
||||
:label="$t('issueReport.contactSupportTitle')"
|
||||
@click="showContactSupport"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="reportOpen">
|
||||
<Divider />
|
||||
@@ -72,6 +79,8 @@ import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.v
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import type { ReportField } from '@/types/issueReportTypes'
|
||||
import {
|
||||
@@ -81,6 +90,8 @@ import {
|
||||
|
||||
import ReportIssuePanel from './error/ReportIssuePanel.vue'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const { error } = defineProps<{
|
||||
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
|
||||
/**
|
||||
@@ -123,6 +134,10 @@ const stackTraceField = computed<ReportField>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const showContactSupport = async () => {
|
||||
await useCommandStore().execute('Comfy.ContactSupport')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
|
||||
@@ -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,25 +47,10 @@
|
||||
<SettingsPanel :setting-groups="sortedGroups(category)" />
|
||||
</PanelTemplate>
|
||||
|
||||
<AboutPanel />
|
||||
<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>
|
||||
@@ -79,16 +64,16 @@ 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 { 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 CurrentUserMessage from './setting/CurrentUserMessage.vue'
|
||||
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
|
||||
@@ -96,24 +81,21 @@ import PanelTemplate from './setting/PanelTemplate.vue'
|
||||
import SettingsPanel from './setting/SettingsPanel.vue'
|
||||
|
||||
const { defaultPanel } = defineProps<{
|
||||
defaultPanel?: 'about' | 'keybinding' | 'extension' | 'server-config'
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
| 'extension'
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| '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 {
|
||||
@@ -125,6 +107,8 @@ const {
|
||||
getSearchResults
|
||||
} = useSettingSearch()
|
||||
|
||||
const authService = useFirebaseAuthService()
|
||||
|
||||
// Sort groups for a category
|
||||
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
|
||||
return [...(category.children ?? [])]
|
||||
@@ -155,6 +139,9 @@ watch(activeCategory, (_, oldValue) => {
|
||||
if (!tabValue.value) {
|
||||
activeCategory.value = oldValue
|
||||
}
|
||||
if (activeCategory.value?.key === 'credits') {
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</a>
|
||||
{{ t('auth.login.andText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/privacy-policy"
|
||||
href="https://www.comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
|
||||
139
src/components/dialog/content/TopUpCreditsDialogContent.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<!-- Balance Section -->
|
||||
<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"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<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>
|
||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||
<Button
|
||||
v-else
|
||||
:label="$t('credits.topUp.buyNow')"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="handleBuyNow(customAmount)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
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'
|
||||
|
||||
const {
|
||||
isInsufficientCredits = false,
|
||||
amountOptions = [5, 10, 20, 50],
|
||||
preselectedAmountOption = 10
|
||||
} = defineProps<{
|
||||
isInsufficientCredits?: boolean
|
||||
amountOptions?: number[]
|
||||
preselectedAmountOption?: number
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authService = useFirebaseAuthService()
|
||||
const customAmount = ref<number>(100)
|
||||
const didClickBuyNow = ref(false)
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const handleSeeDetails = async () => {
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleBuyNow = async (amount: number) => {
|
||||
await authService.purchaseCredits(amount)
|
||||
didClickBuyNow.value = true
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (didClickBuyNow.value) {
|
||||
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Form } from '@primevue/forms'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
@@ -11,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 = [
|
||||
@@ -65,7 +66,12 @@ vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getLogs: vi.fn().mockResolvedValue('mock logs'),
|
||||
getSystemStats: vi.fn().mockResolvedValue('mock stats'),
|
||||
getSettings: vi.fn().mockResolvedValue('mock settings')
|
||||
getSettings: vi.fn().mockResolvedValue('mock settings'),
|
||||
fetchApi: vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue({}),
|
||||
text: vi.fn().mockResolvedValue('')
|
||||
}),
|
||||
apiURL: vi.fn().mockReturnValue('https://test.com')
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -136,15 +142,25 @@ vi.mock('@primevue/forms', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
currentUser: {
|
||||
email: 'test@example.com'
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
describe('ReportIssuePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
})
|
||||
|
||||
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
|
||||
return mount(ReportIssuePanel, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n],
|
||||
plugins: [PrimeVue, i18n, createPinia()],
|
||||
directives: { tooltip: Tooltip }
|
||||
},
|
||||
props,
|
||||
@@ -23,75 +23,136 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 mt-2 border border-round surface-border shadow-1">
|
||||
<div class="flex flex-row gap-3 mb-2">
|
||||
<div v-for="field in fields" :key="field.value">
|
||||
<FormField
|
||||
v-if="field.optIn"
|
||||
v-slot="$field"
|
||||
:name="field.value"
|
||||
class="flex space-x-1"
|
||||
<div class="flex flex-col gap-6">
|
||||
<FormField
|
||||
v-slot="$field"
|
||||
name="contactInfo"
|
||||
:initial-value="authStore.currentUser?.email"
|
||||
>
|
||||
<div class="self-stretch inline-flex justify-start items-center">
|
||||
<label for="contactInfo" class="pb-2 pt-0 opacity-80">{{
|
||||
$t('issueReport.email')
|
||||
}}</label>
|
||||
</div>
|
||||
<InputText
|
||||
id="contactInfo"
|
||||
v-bind="$field"
|
||||
class="w-full"
|
||||
:placeholder="$t('issueReport.provideEmail')"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error && $field.touched && $field.value !== ''"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
<Checkbox
|
||||
{{ t('issueReport.validation.invalidEmail') }}
|
||||
</Message>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="$field" name="helpType">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
class="self-stretch inline-flex justify-start items-center gap-2.5"
|
||||
>
|
||||
<label for="helpType" class="pb-2 pt-0 opacity-80">{{
|
||||
$t('issueReport.whatDoYouNeedHelpWith')
|
||||
}}</label>
|
||||
</div>
|
||||
<Dropdown
|
||||
v-bind="$field"
|
||||
v-model="selection"
|
||||
:input-id="field.value"
|
||||
:value="field.value"
|
||||
v-model="$field.value"
|
||||
:options="helpTypes"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:placeholder="$t('issueReport.selectIssue')"
|
||||
class="w-full"
|
||||
/>
|
||||
<label :for="field.value">{{ field.label }}</label>
|
||||
<Message
|
||||
v-if="$field?.error"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
{{ t('issueReport.validation.selectIssueType') }}
|
||||
</Message>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
class="self-stretch inline-flex justify-start items-center gap-2.5"
|
||||
>
|
||||
<span class="pb-2 pt-0 opacity-80">{{
|
||||
$t('issueReport.whatCanWeInclude')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex flex-row gap-3">
|
||||
<div v-for="field in fields" :key="field.value">
|
||||
<FormField
|
||||
v-if="field.optIn"
|
||||
v-slot="$field"
|
||||
:name="field.value"
|
||||
class="flex space-x-1"
|
||||
>
|
||||
<Checkbox
|
||||
v-bind="$field"
|
||||
v-model="selection"
|
||||
:input-id="field.value"
|
||||
:value="field.value"
|
||||
/>
|
||||
<label :for="field.value">{{ field.label }}</label>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<FormField v-slot="$field" name="details">
|
||||
<div
|
||||
class="self-stretch inline-flex justify-start items-center gap-2.5"
|
||||
>
|
||||
<label for="details" class="pb-2 pt-0 opacity-80">{{
|
||||
$t('issueReport.describeTheProblem')
|
||||
}}</label>
|
||||
</div>
|
||||
<Textarea
|
||||
v-bind="$field"
|
||||
id="details"
|
||||
class="w-full"
|
||||
rows="5"
|
||||
:placeholder="$t('issueReport.provideAdditionalDetails')"
|
||||
:aria-label="$t('issueReport.provideAdditionalDetails')"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error && $field.touched && $field.value"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
{{ t('issueReport.validation.maxLength') }}
|
||||
</Message>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
<FormField v-slot="$field" class="mb-4" name="details">
|
||||
<Textarea
|
||||
v-bind="$field"
|
||||
class="w-full"
|
||||
rows="5"
|
||||
:placeholder="$t('issueReport.provideAdditionalDetails')"
|
||||
:aria-label="$t('issueReport.provideAdditionalDetails')"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error && $field.touched && $field.value"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
{{ t('issueReport.validation.maxLength') }}
|
||||
</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="contactInfo">
|
||||
<InputText
|
||||
v-bind="$field"
|
||||
class="w-full"
|
||||
:placeholder="$t('issueReport.provideEmail')"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error && $field.touched && $field.value !== ''"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
{{ t('issueReport.validation.invalidEmail') }}
|
||||
</Message>
|
||||
</FormField>
|
||||
|
||||
<div class="flex flex-row gap-3 mt-2">
|
||||
<div v-for="checkbox in contactCheckboxes" :key="checkbox.value">
|
||||
<FormField
|
||||
v-slot="$field"
|
||||
:name="checkbox.value"
|
||||
class="flex space-x-1"
|
||||
>
|
||||
<Checkbox
|
||||
v-bind="$field"
|
||||
v-model="contactPrefs"
|
||||
:input-id="checkbox.value"
|
||||
:value="checkbox.value"
|
||||
:disabled="
|
||||
$form.contactInfo?.error || !$form.contactInfo?.value
|
||||
"
|
||||
/>
|
||||
<label :for="checkbox.value">{{ checkbox.label }}</label>
|
||||
</FormField>
|
||||
<div class="flex flex-col gap-3 mt-2">
|
||||
<div v-for="checkbox in contactCheckboxes" :key="checkbox.value">
|
||||
<FormField
|
||||
v-slot="$field"
|
||||
:name="checkbox.value"
|
||||
class="flex space-x-1"
|
||||
>
|
||||
<Checkbox
|
||||
v-bind="$field"
|
||||
v-model="contactPrefs"
|
||||
:input-id="checkbox.value"
|
||||
:value="checkbox.value"
|
||||
:disabled="
|
||||
$form.contactInfo?.error || !$form.contactInfo?.value
|
||||
"
|
||||
/>
|
||||
<label :for="checkbox.value">{{ checkbox.label }}</label>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,6 +169,7 @@ import _ from 'lodash'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import Panel from 'primevue/panel'
|
||||
@@ -122,14 +184,16 @@ import {
|
||||
} from '@/schemas/issueReportSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import type {
|
||||
DefaultField,
|
||||
IssueReportPanelProps,
|
||||
ReportField
|
||||
} from '@/types/issueReportTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
const ISSUE_NAME = 'User reported issue'
|
||||
const DEFAULT_ISSUE_NAME = 'User reported issue'
|
||||
|
||||
const props = defineProps<IssueReportPanelProps>()
|
||||
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
|
||||
@@ -137,6 +201,7 @@ const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const selection = ref<string[]>([])
|
||||
const contactPrefs = ref<string[]>([])
|
||||
@@ -147,6 +212,20 @@ const contactCheckboxes = [
|
||||
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
|
||||
]
|
||||
|
||||
const helpTypes = [
|
||||
{
|
||||
label: t('issueReport.helpTypes.billingPayments'),
|
||||
value: 'billingPayments'
|
||||
},
|
||||
{
|
||||
label: t('issueReport.helpTypes.loginAccessIssues'),
|
||||
value: 'loginAccessIssues'
|
||||
},
|
||||
{ label: t('issueReport.helpTypes.giveFeedback'), value: 'giveFeedback' },
|
||||
{ label: t('issueReport.helpTypes.bugReport'), value: 'bugReport' },
|
||||
{ label: t('issueReport.helpTypes.somethingElse'), value: 'somethingElse' }
|
||||
]
|
||||
|
||||
const defaultFieldsConfig: ReportField[] = [
|
||||
{
|
||||
label: t('issueReport.systemStats'),
|
||||
@@ -213,6 +292,7 @@ const createCaptureContext = async (
|
||||
level: 'error',
|
||||
tags: {
|
||||
errorType: props.errorType,
|
||||
helpType: formData.helpType,
|
||||
followUp: formData.contactInfo ? formData.followUp : false,
|
||||
notifyOnResolution: formData.contactInfo
|
||||
? formData.notifyOnResolution
|
||||
@@ -227,11 +307,24 @@ const createCaptureContext = async (
|
||||
}
|
||||
}
|
||||
|
||||
const generateUniqueTicketId = (type: string) => `${type}-${generateUUID()}`
|
||||
|
||||
const submit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
try {
|
||||
const captureContext = await createCaptureContext(event.values)
|
||||
captureMessage(ISSUE_NAME, captureContext)
|
||||
|
||||
// If it's billing or access issue, generate unique id to be used by customer service ticketing
|
||||
const isValidContactInfo = event.values.contactInfo?.length
|
||||
const isCustomerServiceIssue =
|
||||
isValidContactInfo &&
|
||||
['billingPayments', 'loginAccessIssues'].includes(
|
||||
event.values.helpType || ''
|
||||
)
|
||||
const issueName = isCustomerServiceIssue
|
||||
? `ticket-${generateUniqueTicketId(event.values.helpType || '')}`
|
||||
: DEFAULT_ISSUE_NAME
|
||||
captureMessage(issueName, captureContext)
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
|
||||
@@ -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 = [
|
||||
@@ -10,7 +10,7 @@
|
||||
<ProgressSpinner class="w-8 h-8 mb-2" />
|
||||
{{ $t('manager.loadingVersions') }}
|
||||
</div>
|
||||
<div v-else-if="allVersionOptions.length === 0" class="py-2">
|
||||
<div v-else-if="versionOptions.length === 0" class="py-2">
|
||||
<NoResultsPlaceholder
|
||||
:title="$t('g.noResultsFound')"
|
||||
:message="$t('manager.tryAgainLater')"
|
||||
@@ -23,7 +23,7 @@
|
||||
v-model="selectedVersion"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="allVersionOptions"
|
||||
:options="versionOptions"
|
||||
:highlight-on-select="false"
|
||||
class="my-3 w-full max-h-[50vh] border-none shadow-none"
|
||||
>
|
||||
@@ -58,11 +58,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState, whenever } from '@vueuse/core'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
@@ -119,47 +119,53 @@ const fetchVersions = async () => {
|
||||
return (await registryService.getPackVersions(nodePack.id)) || []
|
||||
}
|
||||
|
||||
const {
|
||||
isLoading: isLoadingVersions,
|
||||
state: versions,
|
||||
execute: startFetchVersions
|
||||
} = useAsyncState(fetchVersions, [])
|
||||
const versionOptions = ref<
|
||||
{
|
||||
value: string
|
||||
label: string
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const specialOptions = computed(() => {
|
||||
const options = [
|
||||
const isLoadingVersions = ref(false)
|
||||
|
||||
const onNodePackChange = async () => {
|
||||
isLoadingVersions.value = true
|
||||
|
||||
// Fetch versions from the registry
|
||||
const versions = await fetchVersions()
|
||||
const availableVersionOptions = versions
|
||||
.map((version) => ({
|
||||
value: version.version ?? '',
|
||||
label: version.version ?? ''
|
||||
}))
|
||||
.filter((option) => option.value)
|
||||
|
||||
// Add Latest option
|
||||
const defaultVersions = [
|
||||
{
|
||||
value: SelectedVersion.LATEST,
|
||||
label: t('manager.latestVersion')
|
||||
}
|
||||
]
|
||||
|
||||
// Only include nightly option if there is a repo
|
||||
// Add Nightly option if there is a non-empty `repository` field
|
||||
if (nodePack.repository?.length) {
|
||||
options.push({
|
||||
defaultVersions.push({
|
||||
value: SelectedVersion.NIGHTLY,
|
||||
label: t('manager.nightlyVersion')
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const versionOptions = computed(() =>
|
||||
versions.value.map((version) => ({
|
||||
value: version.version,
|
||||
label: version.version
|
||||
}))
|
||||
)
|
||||
|
||||
const allVersionOptions = computed(() => [
|
||||
...specialOptions.value,
|
||||
...versionOptions.value
|
||||
])
|
||||
versionOptions.value = [...defaultVersions, ...availableVersionOptions]
|
||||
isLoadingVersions.value = false
|
||||
}
|
||||
|
||||
whenever(
|
||||
() => nodePack.id,
|
||||
() => startFetchVersions(),
|
||||
{ deep: true }
|
||||
() => nodePack,
|
||||
() => {
|
||||
void onNodePackChange()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
||||
@@ -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 = ({
|
||||
164
src/components/dialog/content/setting/CreditsPanel.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<TabPanel value="Credits" class="credits-container h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<h2 class="text-2xl font-bold mb-2">
|
||||
{{ $t('credits.credits') }}
|
||||
</h2>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-sm font-medium text-muted">
|
||||
{{ $t('credits.yourCreditBalance') }}
|
||||
</h3>
|
||||
<div class="flex justify-between items-center">
|
||||
<UserCredit text-class="text-3xl font-bold" />
|
||||
<Skeleton v-if="loading" width="2rem" height="2rem" />
|
||||
<Button
|
||||
v-else
|
||||
:label="$t('credits.purchaseCredits')"
|
||||
:loading="loading"
|
||||
@click="handlePurchaseCreditsClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row items-center">
|
||||
<Skeleton
|
||||
v-if="balanceLoading"
|
||||
width="12rem"
|
||||
height="1rem"
|
||||
class="text-xs"
|
||||
/>
|
||||
<div v-else-if="formattedLastUpdateTime" class="text-xs text-muted">
|
||||
{{ $t('credits.lastUpdated') }}: {{ formattedLastUpdateTime }}
|
||||
</div>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
size="small"
|
||||
severity="secondary"
|
||||
@click="() => authService.fetchBalance()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-8">
|
||||
<Button
|
||||
:label="$t('credits.invoiceHistory')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-up-right"
|
||||
:loading="loading"
|
||||
@click="handleCreditsHistoryClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="creditHistory.length > 0">
|
||||
<div class="flex-grow">
|
||||
<DataTable :value="creditHistory" :show-headers="false">
|
||||
<Column field="title" :header="$t('g.name')">
|
||||
<template #body="{ data }">
|
||||
<div class="text-sm font-medium">{{ data.title }}</div>
|
||||
<div class="text-xs text-muted">{{ data.timestamp }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="amount" :header="$t('g.amount')">
|
||||
<template #body="{ data }">
|
||||
<div
|
||||
:class="[
|
||||
'text-base font-medium text-center',
|
||||
data.isPositive ? 'text-sky-500' : 'text-red-400'
|
||||
]"
|
||||
>
|
||||
{{ data.isPositive ? '+' : '-' }}${{
|
||||
formatMetronomeCurrency(data.amount, 'usd')
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
:label="$t('credits.faqs')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-question-circle"
|
||||
@click="handleFaqClick"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('credits.messageSupport')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-comments"
|
||||
@click="handleMessageSupport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Column from 'primevue/column'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Divider from 'primevue/divider'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
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'
|
||||
|
||||
interface CreditHistoryItemData {
|
||||
title: string
|
||||
timestamp: string
|
||||
amount: number
|
||||
isPositive: boolean
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authService = useFirebaseAuthService()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
const formattedLastUpdateTime = computed(() =>
|
||||
authStore.lastBalanceUpdateTime
|
||||
? authStore.lastBalanceUpdateTime.toLocaleString()
|
||||
: ''
|
||||
)
|
||||
|
||||
const handlePurchaseCreditsClick = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleCreditsHistoryClick = async () => {
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleMessageSupport = () => {
|
||||
dialogService.showIssueReportDialog({
|
||||
title: t('issueReport.contactSupportTitle'),
|
||||
subtitle: t('issueReport.contactSupportDescription'),
|
||||
panelProps: {
|
||||
errorType: 'BillingSupport',
|
||||
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleFaqClick = () => {
|
||||
window.open('https://www.comfy.org/faq', '_blank')
|
||||
}
|
||||
|
||||
const creditHistory = ref<CreditHistoryItemData[]>([])
|
||||
</script>
|
||||
@@ -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,
|
||||
123
src/components/dialog/content/setting/UserPanel.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<TabPanel value="User" class="user-settings-container h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<h2 class="text-2xl font-bold mb-2">{{ $t('userSettings.title') }}</h2>
|
||||
<Divider class="mb-3" />
|
||||
|
||||
<div v-if="user" class="flex flex-col gap-2">
|
||||
<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">
|
||||
{{ $t('userSettings.name') }}
|
||||
</h3>
|
||||
<div class="text-muted">
|
||||
{{ user.displayName || $t('userSettings.notSet') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h3 class="font-medium">
|
||||
{{ $t('userSettings.email') }}
|
||||
</h3>
|
||||
<a :href="'mailto:' + user.email" class="hover:underline">
|
||||
{{ user.email }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h3 class="font-medium">
|
||||
{{ $t('userSettings.provider') }}
|
||||
</h3>
|
||||
<div class="text-muted flex items-center gap-1">
|
||||
<i :class="providerIcon" />
|
||||
{{ providerName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressSpinner
|
||||
v-if="loading"
|
||||
class="w-8 h-8 mt-4"
|
||||
style="--pc-spinner-color: #000"
|
||||
/>
|
||||
<Button
|
||||
v-else
|
||||
class="mt-4 w-32"
|
||||
severity="secondary"
|
||||
:label="$t('auth.signOut.signOut')"
|
||||
icon="pi pi-sign-out"
|
||||
@click="handleSignOut"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Login Section -->
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<p class="text-gray-600">
|
||||
{{ $t('auth.login.title') }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
class="w-52"
|
||||
severity="primary"
|
||||
:loading="loading"
|
||||
:label="$t('auth.login.signInOrSignUp')"
|
||||
icon="pi pi-user"
|
||||
@click="handleSignIn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</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 { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const commandStore = useCommandStore()
|
||||
const user = computed(() => authStore.currentUser)
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const providerName = computed(() => {
|
||||
const providerId = user.value?.providerData[0]?.providerId
|
||||
if (providerId?.includes('google')) {
|
||||
return 'Google'
|
||||
}
|
||||
if (providerId?.includes('github')) {
|
||||
return 'GitHub'
|
||||
}
|
||||
return providerId
|
||||
})
|
||||
|
||||
const providerIcon = computed(() => {
|
||||
const providerId = user.value?.providerData[0]?.providerId
|
||||
if (providerId?.includes('google')) {
|
||||
return 'pi pi-google'
|
||||
}
|
||||
if (providerId?.includes('github')) {
|
||||
return 'pi pi-github'
|
||||
}
|
||||
return 'pi pi-user'
|
||||
})
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await commandStore.execute('Comfy.User.SignOut')
|
||||
}
|
||||
|
||||
const handleSignIn = async () => {
|
||||
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>
|
||||
@@ -57,7 +60,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||
<Button
|
||||
v-else
|
||||
type="submit"
|
||||
:label="t('auth.login.loginButton')"
|
||||
class="h-10 font-medium mt-4"
|
||||
@@ -71,9 +76,17 @@ import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
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()
|
||||
|
||||
@@ -86,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>
|
||||
|
||||
@@ -73,6 +73,7 @@ watch(
|
||||
updateWidgets()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
<TitleEditor />
|
||||
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
|
||||
<canvas
|
||||
id="graph-canvas"
|
||||
@@ -27,13 +26,20 @@
|
||||
tabindex="1"
|
||||
class="w-full h-full touch-none"
|
||||
/>
|
||||
<NodeSearchboxPopover />
|
||||
<SelectionOverlay v-if="selectionToolboxEnabled">
|
||||
<SelectionToolbox />
|
||||
</SelectionOverlay>
|
||||
<NodeTooltip v-if="tooltipEnabled" />
|
||||
|
||||
<NodeBadge />
|
||||
<DomWidgets />
|
||||
<NodeTooltip v-if="tooltipEnabled" />
|
||||
<NodeSearchboxPopover />
|
||||
|
||||
<!-- Initialize components after comfyApp is ready. useAbsolutePosition requires
|
||||
canvasStore.canvas to be initialized. -->
|
||||
<template v-if="comfyAppReady">
|
||||
<TitleEditor />
|
||||
<SelectionOverlay v-if="selectionToolboxEnabled">
|
||||
<SelectionToolbox />
|
||||
</SelectionOverlay>
|
||||
<DomWidgets />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -78,7 +84,9 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const emit = defineEmits<{
|
||||
ready: []
|
||||
}>()
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { LGraphGroup, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { LiteGraphCanvasEvent } from '@comfyorg/litegraph'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
import { type CSSProperties, computed, ref, watch } from 'vue'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
@@ -28,7 +28,12 @@ const settingStore = useSettingStore()
|
||||
|
||||
const showInput = ref(false)
|
||||
const editedTitle = ref('')
|
||||
const { style: inputStyle, updatePosition } = useAbsolutePosition()
|
||||
const { style: inputPositionStyle, updatePosition } = useAbsolutePosition()
|
||||
const inputFontStyle = ref<CSSProperties>({})
|
||||
const inputStyle = computed<CSSProperties>(() => ({
|
||||
...inputPositionStyle.value,
|
||||
...inputFontStyle.value
|
||||
}))
|
||||
|
||||
const titleEditorStore = useTitleEditorStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -59,23 +64,19 @@ watch(
|
||||
|
||||
if (target instanceof LGraphGroup) {
|
||||
const group = target
|
||||
updatePosition(
|
||||
{
|
||||
pos: group.pos,
|
||||
size: [group.size[0], group.titleHeight]
|
||||
},
|
||||
{ fontSize: `${group.font_size * scale}px` }
|
||||
)
|
||||
updatePosition({
|
||||
pos: group.pos,
|
||||
size: [group.size[0], group.titleHeight]
|
||||
})
|
||||
inputFontStyle.value = { fontSize: `${group.font_size * scale}px` }
|
||||
} else if (target instanceof LGraphNode) {
|
||||
const node = target
|
||||
const [x, y] = node.getBounding()
|
||||
updatePosition(
|
||||
{
|
||||
pos: [x, y],
|
||||
size: [node.width, LiteGraph.NODE_TITLE_HEIGHT]
|
||||
},
|
||||
{ fontSize: `${12 * scale}px` }
|
||||
)
|
||||
updatePosition({
|
||||
pos: [x, y],
|
||||
size: [node.width, LiteGraph.NODE_TITLE_HEIGHT]
|
||||
})
|
||||
inputFontStyle.value = { fontSize: `${12 * scale}px` }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -37,13 +37,14 @@ const { widget, widgetState } = defineProps<{
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:widgetValue', value: string | object): void
|
||||
'update:widgetValue': [value: string | object]
|
||||
}>()
|
||||
|
||||
const widgetElement = ref<HTMLElement | undefined>()
|
||||
|
||||
const { style: positionStyle, updatePositionWithTransform } =
|
||||
useAbsolutePosition()
|
||||
const { style: positionStyle, updatePosition } = useAbsolutePosition({
|
||||
useTransform: true
|
||||
})
|
||||
const { style: clippingStyle, updateClipPath } = useDomClipping()
|
||||
const style = computed<CSSProperties>(() => ({
|
||||
...positionStyle.value,
|
||||
@@ -94,7 +95,7 @@ const updateDomClipping = () => {
|
||||
watch(
|
||||
() => widgetState,
|
||||
(newState) => {
|
||||
updatePositionWithTransform(newState)
|
||||
updatePosition(newState)
|
||||
if (enableDomClipping.value) {
|
||||
updateDomClipping()
|
||||
}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
<Slider
|
||||
v-model="lightIntensity"
|
||||
class="w-full"
|
||||
:min="1"
|
||||
:max="20"
|
||||
:step="1"
|
||||
:min="lightIntensityMinimum"
|
||||
:max="lightIntensityMaximum"
|
||||
:step="lightAdjustmentIncrement"
|
||||
@change="updateLightIntensity"
|
||||
/>
|
||||
</div>
|
||||
@@ -38,6 +38,7 @@ import Slider from 'primevue/slider'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
@@ -54,6 +55,16 @@ const lightIntensity = ref(props.lightIntensity)
|
||||
const showLightIntensityButton = ref(props.showLightIntensityButton)
|
||||
const showLightIntensity = ref(false)
|
||||
|
||||
const lightIntensityMaximum = useSettingStore().get(
|
||||
'Comfy.Load3D.LightIntensityMaximum'
|
||||
)
|
||||
const lightIntensityMinimum = useSettingStore().get(
|
||||
'Comfy.Load3D.LightIntensityMinimum'
|
||||
)
|
||||
const lightAdjustmentIncrement = useSettingStore().get(
|
||||
'Comfy.Load3D.LightAdjustmentIncrement'
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.lightIntensity,
|
||||
(newValue) => {
|
||||
|
||||
@@ -33,43 +33,41 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import type {
|
||||
ConnectingLink,
|
||||
LiteGraphCanvasEvent,
|
||||
Vector2
|
||||
import {
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
LiteGraphCanvasEvent
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { OriginalEvent } from '@comfyorg/litegraph/dist/types/events'
|
||||
import { Point } from '@comfyorg/litegraph/dist/interfaces'
|
||||
import type { CanvasPointerEvent } from '@comfyorg/litegraph/dist/types/events'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { computed, ref, toRaw, watchEffect } from 'vue'
|
||||
import { computed, ref, toRaw, watch, watchEffect } from 'vue'
|
||||
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { ConnectingLinkImpl } from '@/types/litegraphTypes'
|
||||
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||
import { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
import NodeSearchBox from './NodeSearchBox.vue'
|
||||
|
||||
let triggerEvent: CanvasPointerEvent | null = null
|
||||
let listenerController: AbortController | null = null
|
||||
let disconnectOnReset = false
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
|
||||
const { visible } = storeToRefs(useSearchBoxStore())
|
||||
const dismissable = ref(true)
|
||||
const triggerEvent = ref<LiteGraphCanvasEvent | null>(null)
|
||||
const getNewNodeLocation = (): Vector2 => {
|
||||
if (!triggerEvent.value) {
|
||||
return litegraphService.getCanvasCenter()
|
||||
}
|
||||
|
||||
const originalEvent = (triggerEvent.value.detail as OriginalEvent)
|
||||
.originalEvent
|
||||
return [originalEvent.canvasX, originalEvent.canvasY]
|
||||
const getNewNodeLocation = (): Point => {
|
||||
return triggerEvent
|
||||
? [triggerEvent.canvasX, triggerEvent.canvasY]
|
||||
: litegraphService.getCanvasCenter()
|
||||
}
|
||||
const nodeFilters = ref<FuseFilterWithValue<ComfyNodeDefImpl, string>[]>([])
|
||||
const addFilter = (filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) => {
|
||||
@@ -88,35 +86,30 @@ const clearFilters = () => {
|
||||
const closeDialog = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const addNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
if (!triggerEvent) {
|
||||
console.warn('The trigger event was undefined when addNode was called.')
|
||||
return
|
||||
}
|
||||
|
||||
disconnectOnReset = false
|
||||
const node = litegraphService.addNodeOnGraph(nodeDef, {
|
||||
pos: getNewNodeLocation()
|
||||
})
|
||||
|
||||
const eventDetail = triggerEvent.value?.detail
|
||||
if (eventDetail && eventDetail.subType === 'empty-release') {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
eventDetail.linkReleaseContext.links.forEach((link: ConnectingLink) => {
|
||||
ConnectingLinkImpl.createFromPlainObject(link).connectTo(node)
|
||||
})
|
||||
}
|
||||
canvasStore.getCanvas().linkConnector.connectToNode(node, triggerEvent)
|
||||
|
||||
// TODO: This is not robust timing-wise.
|
||||
// PrimeVue complains about the dialog being closed before the event selecting
|
||||
// item is fully processed.
|
||||
window.setTimeout(() => {
|
||||
closeDialog()
|
||||
}, 100)
|
||||
window.requestAnimationFrame(closeDialog)
|
||||
}
|
||||
|
||||
const newSearchBoxEnabled = computed(
|
||||
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
|
||||
)
|
||||
const showSearchBox = (e: LiteGraphCanvasEvent) => {
|
||||
const detail = e.detail as OriginalEvent
|
||||
const showSearchBox = (e: CanvasPointerEvent) => {
|
||||
if (newSearchBoxEnabled.value) {
|
||||
if (detail.originalEvent?.pointerType === 'touch') {
|
||||
if (e.pointerType === 'touch') {
|
||||
setTimeout(() => {
|
||||
showNewSearchBox(e)
|
||||
}, 128)
|
||||
@@ -124,26 +117,23 @@ const showSearchBox = (e: LiteGraphCanvasEvent) => {
|
||||
showNewSearchBox(e)
|
||||
}
|
||||
} else {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
canvasStore.canvas.showSearchBox(detail.originalEvent)
|
||||
canvasStore.getCanvas().showSearchBox(e)
|
||||
}
|
||||
}
|
||||
|
||||
const getFirstLink = () =>
|
||||
canvasStore.getCanvas().linkConnector.renderLinks.at(0)
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
|
||||
if (e.detail.subType === 'empty-release') {
|
||||
const links = e.detail.linkReleaseContext.links
|
||||
if (links.length === 0) {
|
||||
console.warn('Empty release with no links! This should never happen')
|
||||
return
|
||||
}
|
||||
const firstLink = ConnectingLinkImpl.createFromPlainObject(links[0])
|
||||
const showNewSearchBox = (e: CanvasPointerEvent) => {
|
||||
const firstLink = getFirstLink()
|
||||
if (firstLink) {
|
||||
const filter =
|
||||
firstLink.releaseSlotType === 'input'
|
||||
firstLink.toType === 'input'
|
||||
? nodeDefStore.nodeSearchService.inputTypeFilter
|
||||
: nodeDefStore.nodeSearchService.outputTypeFilter
|
||||
|
||||
const dataType = firstLink.type?.toString() ?? ''
|
||||
const dataType = firstLink.fromSlot.type?.toString() ?? ''
|
||||
addFilter({
|
||||
filterDef: filter,
|
||||
value: dataType
|
||||
@@ -151,7 +141,7 @@ const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
|
||||
}
|
||||
|
||||
visible.value = true
|
||||
triggerEvent.value = e
|
||||
triggerEvent = e
|
||||
|
||||
// Prevent the dialog from being dismissed immediately
|
||||
dismissable.value = false
|
||||
@@ -160,88 +150,129 @@ const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const showContextMenu = (e: LiteGraphCanvasEvent) => {
|
||||
if (e.detail.subType !== 'empty-release') {
|
||||
return
|
||||
}
|
||||
const showContextMenu = (e: CanvasPointerEvent) => {
|
||||
const firstLink = getFirstLink()
|
||||
if (!firstLink) return
|
||||
|
||||
const links = e.detail.linkReleaseContext.links
|
||||
if (links.length === 0) {
|
||||
console.warn('Empty release with no links! This should never happen')
|
||||
return
|
||||
}
|
||||
|
||||
const firstLink = ConnectingLinkImpl.createFromPlainObject(links[0])
|
||||
const mouseEvent = e.detail.originalEvent
|
||||
const { node, fromSlot, toType } = firstLink
|
||||
const commonOptions = {
|
||||
e: mouseEvent,
|
||||
e,
|
||||
allow_searchbox: true,
|
||||
showSearchBox: () => showSearchBox(e)
|
||||
showSearchBox: () => {
|
||||
cancelResetOnContextClose()
|
||||
showSearchBox(e)
|
||||
}
|
||||
}
|
||||
const connectionOptions = firstLink.output
|
||||
? {
|
||||
nodeFrom: firstLink.node,
|
||||
slotFrom: firstLink.output,
|
||||
afterRerouteId: firstLink.afterRerouteId
|
||||
}
|
||||
: {
|
||||
nodeTo: firstLink.node,
|
||||
slotTo: firstLink.input,
|
||||
afterRerouteId: firstLink.afterRerouteId
|
||||
}
|
||||
// @ts-expect-error fixme ts strict error
|
||||
canvasStore.canvas.showConnectionMenu({
|
||||
const connectionOptions =
|
||||
toType === 'input'
|
||||
? { nodeFrom: node, slotFrom: fromSlot }
|
||||
: { nodeTo: node, slotTo: fromSlot }
|
||||
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const menu = canvas.showConnectionMenu({
|
||||
...connectionOptions,
|
||||
...commonOptions
|
||||
})
|
||||
|
||||
if (!menu) {
|
||||
console.warn('No menu was returned from showConnectionMenu')
|
||||
return
|
||||
}
|
||||
|
||||
triggerEvent = e
|
||||
listenerController = new AbortController()
|
||||
const { signal } = listenerController
|
||||
const options = { once: true, signal }
|
||||
|
||||
// Connect the node after it is created via context menu
|
||||
useEventListener(
|
||||
canvas.canvas,
|
||||
'connect-new-default-node',
|
||||
(createEvent) => {
|
||||
if (!(createEvent instanceof CustomEvent))
|
||||
throw new Error('Invalid event')
|
||||
|
||||
const node: unknown = createEvent.detail?.node
|
||||
if (!(node instanceof LGraphNode)) throw new Error('Invalid node')
|
||||
|
||||
disconnectOnReset = false
|
||||
createEvent.preventDefault()
|
||||
canvas.linkConnector.connectToNode(node, e)
|
||||
},
|
||||
options
|
||||
)
|
||||
|
||||
// Reset when the context menu is closed
|
||||
const cancelResetOnContextClose = useEventListener(
|
||||
menu.controller.signal,
|
||||
'abort',
|
||||
reset,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
// Disable litegraph's default behavior of release link and search box.
|
||||
const canvasStore = useCanvasStore()
|
||||
watchEffect(() => {
|
||||
if (canvasStore.canvas) {
|
||||
LiteGraph.release_link_on_empty_shows_menu = false
|
||||
canvasStore.canvas.allow_searchbox = false
|
||||
}
|
||||
const { canvas } = canvasStore
|
||||
if (!canvas) return
|
||||
|
||||
LiteGraph.release_link_on_empty_shows_menu = false
|
||||
canvas.allow_searchbox = false
|
||||
|
||||
useEventListener(
|
||||
canvas.linkConnector.events,
|
||||
'dropped-on-canvas',
|
||||
handleDroppedOnCanvas
|
||||
)
|
||||
})
|
||||
|
||||
const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
|
||||
if (e.detail.subType === 'empty-double-click') {
|
||||
showSearchBox(e)
|
||||
} else if (e.detail.subType === 'empty-release') {
|
||||
handleCanvasEmptyRelease(e)
|
||||
showSearchBox(e.detail.originalEvent)
|
||||
} else if (e.detail.subType === 'group-double-click') {
|
||||
const group = e.detail.group
|
||||
const [_, y] = group.pos
|
||||
const relativeY = e.detail.originalEvent.canvasY - y
|
||||
// Show search box if the click is NOT on the title bar
|
||||
if (relativeY > group.titleHeight) {
|
||||
showSearchBox(e)
|
||||
showSearchBox(e.detail.originalEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const linkReleaseAction = computed(() => {
|
||||
return settingStore.get('Comfy.LinkRelease.Action')
|
||||
})
|
||||
const linkReleaseAction = computed(() =>
|
||||
settingStore.get('Comfy.LinkRelease.Action')
|
||||
)
|
||||
|
||||
const linkReleaseActionShift = computed(() => {
|
||||
return settingStore.get('Comfy.LinkRelease.ActionShift')
|
||||
})
|
||||
const linkReleaseActionShift = computed(() =>
|
||||
settingStore.get('Comfy.LinkRelease.ActionShift')
|
||||
)
|
||||
|
||||
const handleCanvasEmptyRelease = (e: LiteGraphCanvasEvent) => {
|
||||
const detail = e.detail as OriginalEvent
|
||||
const shiftPressed = detail.originalEvent.shiftKey
|
||||
// Prevent normal LinkConnector reset (called by CanvasPointer.finally)
|
||||
const preventDefault = (e: Event) => e.preventDefault()
|
||||
const cancelNextReset = (e: CustomEvent<CanvasPointerEvent>) => {
|
||||
e.preventDefault()
|
||||
|
||||
const action = shiftPressed
|
||||
const canvas = canvasStore.getCanvas()
|
||||
canvas.linkConnector.state.snapLinksPos = [e.detail.canvasX, e.detail.canvasY]
|
||||
useEventListener(canvas.linkConnector.events, 'reset', preventDefault, {
|
||||
once: true
|
||||
})
|
||||
}
|
||||
|
||||
const handleDroppedOnCanvas = (e: CustomEvent<CanvasPointerEvent>) => {
|
||||
disconnectOnReset = true
|
||||
const action = e.detail.shiftKey
|
||||
? linkReleaseActionShift.value
|
||||
: linkReleaseAction.value
|
||||
switch (action) {
|
||||
case LinkReleaseTriggerAction.SEARCH_BOX:
|
||||
showSearchBox(e)
|
||||
cancelNextReset(e)
|
||||
showSearchBox(e.detail)
|
||||
break
|
||||
case LinkReleaseTriggerAction.CONTEXT_MENU:
|
||||
showContextMenu(e)
|
||||
cancelNextReset(e)
|
||||
showContextMenu(e.detail)
|
||||
break
|
||||
case LinkReleaseTriggerAction.NO_ACTION:
|
||||
default:
|
||||
@@ -249,6 +280,25 @@ const handleCanvasEmptyRelease = (e: LiteGraphCanvasEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Resets litegraph state
|
||||
const reset = () => {
|
||||
listenerController?.abort()
|
||||
listenerController = null
|
||||
triggerEvent = null
|
||||
|
||||
const canvas = canvasStore.getCanvas()
|
||||
canvas.linkConnector.events.removeEventListener('reset', preventDefault)
|
||||
if (disconnectOnReset) canvas.linkConnector.disconnectLinks()
|
||||
|
||||
canvas.linkConnector.reset()
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
// Reset connecting links when the search box is closed
|
||||
watch(visible, () => {
|
||||
if (!visible.value) reset()
|
||||
})
|
||||
|
||||
useEventListener(document, 'litegraph:canvas', canvasEventHandler)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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,8 +1,5 @@
|
||||
<template>
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.modelLibrary')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.modelLibrary')">
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('g.refresh')"
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<SidebarTabTemplate
|
||||
:title="$t('sideToolbar.nodeLibrary')"
|
||||
class="bg-[var(--p-tree-background)]"
|
||||
>
|
||||
<SidebarTabTemplate :title="$t('sideToolbar.nodeLibrary')">
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('g.newFolder')"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="comfy-vue-side-bar-container flex flex-col h-full group/sidebar-tab"
|
||||
class="comfy-vue-side-bar-container flex flex-col h-full group/sidebar-tab bg-[var(--p-tree-background)]"
|
||||
:class="props.class"
|
||||
>
|
||||
<div class="comfy-vue-side-bar-header">
|
||||
|
||||
49
src/components/topbar/CurrentUserButton.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<!-- A button that shows current authenticated user's avatar -->
|
||||
<template>
|
||||
<div>
|
||||
<Button
|
||||
v-if="isAuthenticated"
|
||||
class="user-profile-button p-1"
|
||||
severity="secondary"
|
||||
text
|
||||
aria-label="user profile"
|
||||
@click="popover?.toggle($event)"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
|
||||
<i class="pi pi-chevron-down px-1" :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 Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
import CurrentUserPopover from './CurrentUserPopover.vue'
|
||||
|
||||
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>
|
||||
@@ -12,6 +12,7 @@
|
||||
</div>
|
||||
<div ref="menuRight" class="comfyui-menu-right flex-shrink-0" />
|
||||
<Actionbar />
|
||||
<CurrentUserButton class="flex-shrink-0" />
|
||||
<BottomPanelToggleButton class="flex-shrink-0" />
|
||||
<Button
|
||||
v-tooltip="{ value: $t('menu.hideMenu'), showDelay: 300 }"
|
||||
@@ -44,6 +45,7 @@ import { computed, onMounted, provide, ref } from 'vue'
|
||||
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import BottomPanelToggleButton from '@/components/topbar/BottomPanelToggleButton.vue'
|
||||
import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
@@ -57,6 +59,7 @@ import {
|
||||
|
||||
const workspaceState = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const workflowTabsPosition = computed(() =>
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Size, Vector2 } from '@comfyorg/litegraph'
|
||||
import { CSSProperties, ref } from 'vue'
|
||||
import { CSSProperties, computed, ref } from 'vue'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
export interface PositionConfig {
|
||||
@@ -13,70 +13,56 @@ export interface PositionConfig {
|
||||
scale?: number
|
||||
}
|
||||
|
||||
export function useAbsolutePosition() {
|
||||
export function useAbsolutePosition(options: { useTransform?: boolean } = {}) {
|
||||
const { useTransform = false } = options
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const style = ref<CSSProperties>({
|
||||
position: 'fixed',
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
width: '0px',
|
||||
height: '0px'
|
||||
const lgCanvas = canvasStore.getCanvas()
|
||||
const { canvasPosToClientPos } = useCanvasPositionConversion(
|
||||
lgCanvas.canvas,
|
||||
lgCanvas
|
||||
)
|
||||
|
||||
const position = ref<PositionConfig>({
|
||||
pos: [0, 0],
|
||||
size: [0, 0]
|
||||
})
|
||||
|
||||
const style = computed<CSSProperties>(() => {
|
||||
const { pos, size, scale = lgCanvas.ds.scale } = position.value
|
||||
const [left, top] = canvasPosToClientPos(pos)
|
||||
const [width, height] = size
|
||||
|
||||
return useTransform
|
||||
? {
|
||||
position: 'fixed',
|
||||
transformOrigin: '0 0',
|
||||
transform: `scale(${scale})`,
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`
|
||||
}
|
||||
: {
|
||||
position: 'fixed',
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: `${width * scale}px`,
|
||||
height: `${height * scale}px`
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Update the position of the element on the litegraph canvas.
|
||||
*
|
||||
* @param config
|
||||
* @param extraStyle
|
||||
*/
|
||||
const updatePosition = (
|
||||
config: PositionConfig,
|
||||
extraStyle?: CSSProperties
|
||||
) => {
|
||||
const { pos, size, scale = canvasStore.canvas?.ds?.scale ?? 1 } = config
|
||||
const [left, top] = app.canvasPosToClientPos(pos)
|
||||
const [width, height] = size
|
||||
|
||||
style.value = {
|
||||
...style.value,
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: `${width * scale}px`,
|
||||
height: `${height * scale}px`,
|
||||
...extraStyle
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position and size of the element on the litegraph canvas,
|
||||
* with CSS transform scaling applied.
|
||||
*
|
||||
* @param config
|
||||
* @param extraStyle
|
||||
*/
|
||||
const updatePositionWithTransform = (
|
||||
config: PositionConfig,
|
||||
extraStyle?: CSSProperties
|
||||
) => {
|
||||
const { pos, size, scale = canvasStore.canvas?.ds?.scale ?? 1 } = config
|
||||
const [left, top] = app.canvasPosToClientPos(pos)
|
||||
const [width, height] = size
|
||||
|
||||
style.value = {
|
||||
...style.value,
|
||||
transformOrigin: '0 0',
|
||||
transform: `scale(${scale})`,
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
...extraStyle
|
||||
}
|
||||
const updatePosition = (config: PositionConfig) => {
|
||||
position.value = config
|
||||
}
|
||||
|
||||
return {
|
||||
style,
|
||||
updatePosition,
|
||||
updatePositionWithTransform
|
||||
updatePosition
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ export const useCanvasPositionConversion = (
|
||||
const clientPosToCanvasPos = (pos: Vector2): Vector2 => {
|
||||
const { offset, scale } = lgCanvas.ds
|
||||
return [
|
||||
(pos[0] - left.value) / scale + offset[0],
|
||||
(pos[1] - top.value) / scale + offset[1]
|
||||
(pos[0] - left.value) / scale - offset[0],
|
||||
(pos[1] - top.value) / scale - offset[1]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
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'
|
||||
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
|
||||
import type { SettingParams } from '@/types/settingTypes'
|
||||
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' | 'keybinding' | 'extension' | 'server-config'
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
| 'extension'
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
) {
|
||||
const { t } = useI18n()
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const settingStore = useSettingStore()
|
||||
const activeCategory = ref<SettingTreeNode | null>(null)
|
||||
|
||||
@@ -40,46 +59,94 @@ 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 keybindingPanelNode: SettingTreeNode = {
|
||||
key: 'keybinding',
|
||||
label: 'Keybinding',
|
||||
children: []
|
||||
const creditsPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'credits',
|
||||
label: 'Credits',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/CreditsPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const extensionPanelNode: SettingTreeNode = {
|
||||
key: 'extension',
|
||||
label: 'Extension',
|
||||
children: []
|
||||
const userPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'user',
|
||||
label: 'User',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/UserPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const serverConfigPanelNode: SettingTreeNode = {
|
||||
key: 'server-config',
|
||||
label: 'Server-Config',
|
||||
children: []
|
||||
const keybindingPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'keybinding',
|
||||
label: 'Keybinding',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/KeybindingPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Server config panel is only available in Electron
|
||||
*/
|
||||
const serverConfigPanelNodeList = computed<SettingTreeNode[]>(() => {
|
||||
return isElectron() ? [serverConfigPanelNode] : []
|
||||
})
|
||||
const extensionPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'extension',
|
||||
label: 'Extension',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/ExtensionPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const serverConfigPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'server-config',
|
||||
label: 'Server-Config',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/ServerConfigPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const panels = computed<SettingPanelItem[]>(() =>
|
||||
[
|
||||
aboutPanel,
|
||||
creditsPanel,
|
||||
userPanel,
|
||||
keybindingPanel,
|
||||
extensionPanel
|
||||
].filter((panel) => panel.component)
|
||||
)
|
||||
|
||||
/**
|
||||
* The default category to show when the dialog is opened.
|
||||
*/
|
||||
const defaultCategory = computed<SettingTreeNode>(() => {
|
||||
return defaultPanel
|
||||
? settingCategories.value.find((x) => x.key === defaultPanel) ??
|
||||
settingCategories.value[0]
|
||||
: settingCategories.value[0]
|
||||
if (!defaultPanel) return settingCategories.value[0]
|
||||
// Search through all groups in groupedMenuTreeNodes
|
||||
for (const group of groupedMenuTreeNodes.value) {
|
||||
const found = group.children?.find((node) => node.key === defaultPanel)
|
||||
if (found) return found
|
||||
}
|
||||
return settingCategories.value[0]
|
||||
})
|
||||
|
||||
const translateCategory = (node: SettingTreeNode) => ({
|
||||
@@ -91,6 +158,15 @@ export function useSettingUI(
|
||||
})
|
||||
|
||||
const groupedMenuTreeNodes = computed<SettingTreeNode[]>(() => [
|
||||
// Account settings - only show credits when user is authenticated
|
||||
{
|
||||
key: 'account',
|
||||
label: 'Account',
|
||||
children: [
|
||||
userPanel.node,
|
||||
...(firebaseAuthStore.isAuthenticated ? [creditsPanel.node] : [])
|
||||
].map(translateCategory)
|
||||
},
|
||||
// Normal settings stored in the settingStore
|
||||
{
|
||||
key: 'settings',
|
||||
@@ -102,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)
|
||||
}
|
||||
])
|
||||
@@ -115,6 +191,7 @@ export function useSettingUI(
|
||||
})
|
||||
|
||||
return {
|
||||
panels,
|
||||
activeCategory,
|
||||
defaultCategory,
|
||||
groupedMenuTreeNodes,
|
||||
|
||||
@@ -29,12 +29,10 @@ export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement>) => {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
// Add an offset on y to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX,
|
||||
loc.clientY + LiteGraph.NODE_TITLE_HEIGHT
|
||||
])
|
||||
pos[1] += LiteGraph.NODE_TITLE_HEIGHT
|
||||
litegraphService.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
|
||||
@@ -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'),
|
||||
@@ -579,6 +580,22 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ContactSupport',
|
||||
icon: 'pi pi-question',
|
||||
label: 'Contact Support',
|
||||
versionAdded: '1.17.8',
|
||||
function: () => {
|
||||
dialogService.showIssueReportDialog({
|
||||
title: t('issueReport.contactSupportTitle'),
|
||||
subtitle: t('issueReport.contactSupportDescription'),
|
||||
panelProps: {
|
||||
errorType: 'ContactSupport',
|
||||
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyUIForum',
|
||||
icon: 'pi pi-comments',
|
||||
@@ -616,8 +633,26 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
dialogService.showManagerProgressDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.User.OpenSignInDialog',
|
||||
icon: 'pi pi-user',
|
||||
label: 'Open Sign In Dialog',
|
||||
versionAdded: '1.17.6',
|
||||
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' }))
|
||||
}
|
||||
|
||||
@@ -38,6 +38,15 @@ export const usePaste = () => {
|
||||
}
|
||||
|
||||
useEventListener(document, 'paste', async (e) => {
|
||||
const isTargetInGraph =
|
||||
e.target instanceof Element &&
|
||||
(e.target.classList.contains('litegraph') ||
|
||||
e.target.classList.contains('graph-canvas-container') ||
|
||||
e.target.id === 'graph-canvas')
|
||||
|
||||
// If the target is not in the graph, we don't want to handle the paste event
|
||||
if (!isTargetInGraph) return
|
||||
|
||||
// ctrl+shift+v is used to paste nodes with connections
|
||||
// this is handled by litegraph
|
||||
if (workspaceStore.shiftDown) return
|
||||
|
||||
3
src/config/comfyApi.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const COMFY_API_BASE_URL = __USE_PROD_CONFIG__
|
||||
? 'https://api.comfy.org'
|
||||
: 'https://stagingapi.comfy.org'
|
||||
@@ -22,8 +22,7 @@ const PROD_CONFIG: FirebaseOptions = {
|
||||
measurementId: 'G-3ZBD3MBTG4'
|
||||
}
|
||||
|
||||
// To test with prod config while using dev server, set USE_PROD_FIREBASE_CONFIG=true in .env
|
||||
// Otherwise, build with `npm run build` the and set `--front-end-root` to `ComfyUI_frontend/dist`
|
||||
export const FIREBASE_CONFIG: FirebaseOptions = __USE_PROD_FIREBASE_CONFIG__
|
||||
// To test with prod config while using dev server, set USE_PROD_CONFIG=true in .env
|
||||
export const FIREBASE_CONFIG: FirebaseOptions = __USE_PROD_CONFIG__
|
||||
? PROD_CONFIG
|
||||
: DEV_CONFIG
|
||||
|
||||
@@ -23,5 +23,8 @@ export const CORE_MENU_COMMANDS = [
|
||||
'Comfy.Help.OpenComfyUIForum'
|
||||
]
|
||||
],
|
||||
[['Help'], ['Comfy.Help.AboutComfyUI', 'Comfy.Feedback']]
|
||||
[
|
||||
['Help'],
|
||||
['Comfy.Help.AboutComfyUI', 'Comfy.Feedback', 'Comfy.ContactSupport']
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -38,6 +38,16 @@ useExtensionService().registerExtension({
|
||||
defaultValue: true,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.BackgroundColor',
|
||||
category: ['3D', 'Scene', 'Initial Background Color'],
|
||||
name: 'Initial Background Color',
|
||||
tooltip:
|
||||
'Controls the default background color of the 3D scene. This setting determines the background appearance when a new 3D widget is created, but can be adjusted individually for each widget after creation.',
|
||||
type: 'color',
|
||||
defaultValue: '282828',
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.CameraType',
|
||||
category: ['3D', 'Camera', 'Initial Camera Type'],
|
||||
@@ -48,6 +58,51 @@ useExtensionService().registerExtension({
|
||||
options: ['perspective', 'orthographic'],
|
||||
defaultValue: 'perspective',
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.LightIntensity',
|
||||
category: ['3D', 'Light', 'Initial Light Intensity'],
|
||||
name: 'Initial Light Intensity',
|
||||
tooltip:
|
||||
'Sets the default brightness level of lighting in the 3D scene. This value determines how intensely lights illuminate objects when a new 3D widget is created, but can be adjusted individually for each widget after creation.',
|
||||
type: 'number',
|
||||
defaultValue: 3,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.LightIntensityMaximum',
|
||||
category: ['3D', 'Light', 'Light Intensity Maximum'],
|
||||
name: 'Light Intensity Maximum',
|
||||
tooltip:
|
||||
'Sets the maximum allowable light intensity value for 3D scenes. This defines the upper brightness limit that can be set when adjusting lighting in any 3D widget.',
|
||||
type: 'number',
|
||||
defaultValue: 10,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.LightIntensityMinimum',
|
||||
category: ['3D', 'Light', 'Light Intensity Minimum'],
|
||||
name: 'Light Intensity Minimum',
|
||||
tooltip:
|
||||
'Sets the minimum allowable light intensity value for 3D scenes. This defines the lower brightness limit that can be set when adjusting lighting in any 3D widget.',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.LightAdjustmentIncrement',
|
||||
category: ['3D', 'Light', 'Light Adjustment Increment'],
|
||||
name: 'Light Adjustment Increment',
|
||||
tooltip:
|
||||
'Controls the increment size when adjusting light intensity in 3D scenes. A smaller step value allows for finer control over lighting adjustments, while a larger value results in more noticeable changes per adjustment.',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.1
|
||||
},
|
||||
defaultValue: 0.5,
|
||||
experimental: true
|
||||
}
|
||||
],
|
||||
getCustomWidgets() {
|
||||
|
||||
@@ -91,12 +91,18 @@ class Load3DConfiguration {
|
||||
|
||||
this.load3d.togglePreview(showPreview)
|
||||
|
||||
const bgColor = this.load3d.loadNodeProperty('Background Color', '#282828')
|
||||
const bgColor = this.load3d.loadNodeProperty(
|
||||
'Background Color',
|
||||
'#' + useSettingStore().get('Comfy.Load3D.BackgroundColor')
|
||||
)
|
||||
|
||||
this.load3d.setBackgroundColor(bgColor)
|
||||
|
||||
const lightIntensity: number = Number(
|
||||
this.load3d.loadNodeProperty('Light Intensity', 5)
|
||||
this.load3d.loadNodeProperty(
|
||||
'Light Intensity',
|
||||
useSettingStore().get('Comfy.Load3D.LightIntensity')
|
||||
)
|
||||
)
|
||||
|
||||
this.load3d.setLightIntensity(lightIntensity)
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -18,16 +18,6 @@ import { mergeInputSpec } from '@/utils/nodeDefUtil'
|
||||
import { applyTextReplacements } from '@/utils/searchAndReplace'
|
||||
import { isPrimitiveNode } from '@/utils/typeGuardUtil'
|
||||
|
||||
const VALID_TYPES = [
|
||||
'STRING',
|
||||
'combo',
|
||||
'number',
|
||||
'toggle',
|
||||
'BOOLEAN',
|
||||
'text',
|
||||
'string'
|
||||
]
|
||||
|
||||
const replacePropertyName = 'Run widget replace on values'
|
||||
export class PrimitiveNode extends LGraphNode {
|
||||
controlValues?: any[]
|
||||
@@ -352,31 +342,6 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
}
|
||||
|
||||
isValidWidgetLink(
|
||||
originSlot: number,
|
||||
targetNode: LGraphNode,
|
||||
targetWidget: IWidget
|
||||
) {
|
||||
const config2 = getConfig.call(targetNode, targetWidget.name) ?? [
|
||||
targetWidget.type,
|
||||
targetWidget.options || {}
|
||||
]
|
||||
if (!isConvertibleWidget(targetWidget, config2)) return false
|
||||
|
||||
const output = this.outputs[originSlot]
|
||||
if (
|
||||
!(
|
||||
output.widget?.[CONFIG] ??
|
||||
(output.widget?.[GET_CONFIG] as () => InputSpec)?.()
|
||||
)
|
||||
) {
|
||||
// No widget defined for this primitive yet so allow it
|
||||
return true
|
||||
}
|
||||
|
||||
return !!mergeIfValid.call(this, output, config2)
|
||||
}
|
||||
|
||||
#isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
|
||||
// Only allow connections where the configs match
|
||||
const output = this.outputs?.[0]
|
||||
@@ -445,13 +410,6 @@ function getConfig(this: LGraphNode, widgetName: string) {
|
||||
)
|
||||
}
|
||||
|
||||
function isConvertibleWidget(widget: IWidget, config: InputSpec): boolean {
|
||||
return (
|
||||
// @ts-expect-error InputSpec is not typed correctly
|
||||
VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a widget to an input slot.
|
||||
* @deprecated Widget to socket conversion is no longer necessary, as they co-exist now.
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "Clear Workflow"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Contact Support"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Duplicate Current Workflow"
|
||||
},
|
||||
@@ -173,6 +176,12 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Undo"
|
||||
},
|
||||
"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.",
|
||||
@@ -110,7 +111,9 @@
|
||||
"migrate": "Migrate",
|
||||
"updateAvailable": "Update Available",
|
||||
"login": "Login",
|
||||
"learnMore": "Learn more"
|
||||
"learnMore": "Learn more",
|
||||
"amount": "Amount",
|
||||
"unknownError": "Unknown error"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -180,9 +183,24 @@
|
||||
"helpFix": "Help Fix This",
|
||||
"rating": "Rating",
|
||||
"feedbackTitle": "Help us improve ComfyUI by providing feedback",
|
||||
"contactSupportTitle": "Contact Support",
|
||||
"contactSupportDescription": "Please fill in the form below with your report",
|
||||
"selectIssue": "Select the issue",
|
||||
"whatDoYouNeedHelpWith": "What do you need help with?",
|
||||
"whatCanWeInclude": "Specify what to include in the report",
|
||||
"describeTheProblem": "Describe the problem",
|
||||
"email": "Email",
|
||||
"helpTypes": {
|
||||
"billingPayments": "Billing / Payments",
|
||||
"loginAccessIssues": "Login / Access Issues",
|
||||
"giveFeedback": "Give Feedback",
|
||||
"bugReport": "Bug Report",
|
||||
"somethingElse": "Something Else"
|
||||
},
|
||||
"validation": {
|
||||
"maxLength": "Message too long",
|
||||
"invalidEmail": "Please enter a valid email address"
|
||||
"invalidEmail": "Please enter a valid email address",
|
||||
"selectIssueType": "Please select an issue type"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
@@ -630,6 +648,7 @@
|
||||
"Zoom Out": "Zoom Out",
|
||||
"Clear Pending Tasks": "Clear Pending Tasks",
|
||||
"Clear Workflow": "Clear Workflow",
|
||||
"Contact Support": "Contact Support",
|
||||
"Duplicate Current Workflow": "Duplicate Current Workflow",
|
||||
"Export": "Export",
|
||||
"Export (API)": "Export (API)",
|
||||
@@ -660,6 +679,8 @@
|
||||
"Show Settings Dialog": "Show Settings Dialog",
|
||||
"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",
|
||||
@@ -725,7 +746,10 @@
|
||||
"Load 3D": "Load 3D",
|
||||
"Camera": "Camera",
|
||||
"Scene": "Scene",
|
||||
"3D": "3D"
|
||||
"3D": "3D",
|
||||
"Light": "Light",
|
||||
"User": "User",
|
||||
"Credits": "Credits"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -1055,11 +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",
|
||||
@@ -1091,6 +1126,11 @@
|
||||
"signIn": "Sign in",
|
||||
"signUpWithGoogle": "Sign up with Google",
|
||||
"signUpWithGithub": "Sign up with Github"
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Log Out",
|
||||
"success": "Signed out successfully",
|
||||
"successDetail": "You have been signed out of your account."
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
@@ -1107,5 +1147,34 @@
|
||||
"special": "Must contain at least one special character",
|
||||
"match": "Passwords must match"
|
||||
}
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Credits",
|
||||
"yourCreditBalance": "Your credit balance",
|
||||
"purchaseCredits": "Purchase Credits",
|
||||
"invoiceHistory": "Invoice History",
|
||||
"faqs": "FAQs",
|
||||
"messageSupport": "Message Support",
|
||||
"lastUpdated": "Last updated",
|
||||
"topUp": {
|
||||
"insufficientTitle": "Insufficient Credits",
|
||||
"insufficientMessage": "You don't have enough credits to run this workflow.",
|
||||
"quickPurchase": "Quick Purchase",
|
||||
"maxAmount": "(Max. $1,000 USD)",
|
||||
"buyNow": "Buy now",
|
||||
"seeDetails": "See details",
|
||||
"topUp": "Top Up"
|
||||
}
|
||||
},
|
||||
"userSettings": {
|
||||
"title": "User Settings",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"notSet": "Not set",
|
||||
"provider": "Sign in method",
|
||||
"providers": {
|
||||
"google": "Google",
|
||||
"github": "GitHub"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,10 @@
|
||||
"Hidden": "Hidden"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Initial Background Color",
|
||||
"tooltip": "Controls the default background color of the 3D scene. This setting determines the background appearance when a new 3D widget is created, but can be adjusted individually for each widget after creation."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Initial Camera Type",
|
||||
"tooltip": "Controls whether the camera is perspective or orthographic by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.",
|
||||
@@ -116,6 +120,22 @@
|
||||
"orthographic": "orthographic"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "Light Adjustment Increment",
|
||||
"tooltip": "Controls the increment size when adjusting light intensity in 3D scenes. A smaller step value allows for finer control over lighting adjustments, while a larger value results in more noticeable changes per adjustment."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "Initial Light Intensity",
|
||||
"tooltip": "Sets the default brightness level of lighting in the 3D scene. This value determines how intensely lights illuminate objects when a new 3D widget is created, but can be adjusted individually for each widget after creation."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "Light Intensity Maximum",
|
||||
"tooltip": "Sets the maximum allowable light intensity value for 3D scenes. This defines the upper brightness limit that can be set when adjusting lighting in any 3D widget."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "Light Intensity Minimum",
|
||||
"tooltip": "Sets the minimum allowable light intensity value for 3D scenes. This defines the lower brightness limit that can be set when adjusting lighting in any 3D widget."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Initial Grid Visibility",
|
||||
"tooltip": "Controls whether the grid is visible by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation."
|
||||
@@ -273,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."
|
||||
@@ -357,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"
|
||||
},
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "Borrar flujo de trabajo"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Contactar soporte"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Duplicar flujo de trabajo actual"
|
||||
},
|
||||
@@ -173,6 +176,12 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Deshacer"
|
||||
},
|
||||
"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,6 +17,7 @@
|
||||
"emailPlaceholder": "Ingresa tu correo electrónico",
|
||||
"failed": "Inicio de sesión fallido",
|
||||
"forgotPassword": "¿Olvidaste tu contraseña?",
|
||||
"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",
|
||||
@@ -24,13 +25,21 @@
|
||||
"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",
|
||||
"success": "Inicio de sesión exitoso",
|
||||
"termsLink": "Términos de uso",
|
||||
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
|
||||
"title": "Inicia sesión en tu cuenta"
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Cerrar sesión",
|
||||
"success": "Sesión cerrada correctamente",
|
||||
"successDetail": "Has cerrado sesión en tu cuenta."
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "¿Ya tienes una cuenta?",
|
||||
"emailLabel": "Correo electrónico",
|
||||
@@ -92,6 +101,24 @@
|
||||
"Title": "Título",
|
||||
"Unpin": "Desanclar"
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Créditos",
|
||||
"faqs": "Preguntas frecuentes",
|
||||
"invoiceHistory": "Historial de facturas",
|
||||
"lastUpdated": "Última actualización",
|
||||
"messageSupport": "Contactar soporte",
|
||||
"purchaseCredits": "Comprar créditos",
|
||||
"topUp": {
|
||||
"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)",
|
||||
"quickPurchase": "Compra rápida",
|
||||
"seeDetails": "Ver detalles",
|
||||
"topUp": "Recargar"
|
||||
},
|
||||
"yourCreditBalance": "Tu saldo de créditos"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "AUDIO",
|
||||
"BOOLEAN": "BOOLEANO",
|
||||
@@ -168,6 +195,7 @@
|
||||
"about": "Acerca de",
|
||||
"add": "Añadir",
|
||||
"all": "Todo",
|
||||
"amount": "Cantidad",
|
||||
"apply": "Aplicar",
|
||||
"back": "Atrás",
|
||||
"cancel": "Cancelar",
|
||||
@@ -269,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"
|
||||
},
|
||||
@@ -392,19 +422,34 @@
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Contáctame para seguimiento",
|
||||
"contactSupportDescription": "Por favor, complete el siguiente formulario con su reporte",
|
||||
"contactSupportTitle": "Contactar Soporte",
|
||||
"describeTheProblem": "Describa el problema",
|
||||
"email": "Correo electrónico",
|
||||
"feedbackTitle": "Ayúdanos a mejorar ComfyUI proporcionando comentarios",
|
||||
"helpFix": "Ayuda a Solucionar Esto",
|
||||
"helpTypes": {
|
||||
"billingPayments": "Facturación / Pagos",
|
||||
"bugReport": "Reporte de error",
|
||||
"giveFeedback": "Enviar comentarios",
|
||||
"loginAccessIssues": "Problemas de inicio de sesión / acceso",
|
||||
"somethingElse": "Otro"
|
||||
},
|
||||
"notifyResolve": "Notifícame cuando se resuelva",
|
||||
"provideAdditionalDetails": "Proporciona detalles adicionales (opcional)",
|
||||
"provideEmail": "Danos tu correo electrónico (opcional)",
|
||||
"rating": "Calificación",
|
||||
"selectIssue": "Seleccione el problema",
|
||||
"stackTrace": "Rastreo de Pila",
|
||||
"submitErrorReport": "Enviar Reporte de Error (Opcional)",
|
||||
"systemStats": "Estadísticas del Sistema",
|
||||
"validation": {
|
||||
"invalidEmail": "Por favor ingresa una dirección de correo electrónico válida",
|
||||
"maxLength": "Mensaje demasiado largo"
|
||||
}
|
||||
"maxLength": "Mensaje demasiado largo",
|
||||
"selectIssueType": "Por favor, seleccione un tipo de problema"
|
||||
},
|
||||
"whatCanWeInclude": "Especifique qué incluir en el reporte",
|
||||
"whatDoYouNeedHelpWith": "¿Con qué necesita ayuda?"
|
||||
},
|
||||
"load3d": {
|
||||
"applyingTexture": "Aplicando textura...",
|
||||
@@ -577,6 +622,7 @@
|
||||
"ComfyUI Docs": "Documentos de ComfyUI",
|
||||
"ComfyUI Forum": "Foro de ComfyUI",
|
||||
"ComfyUI Issues": "Problemas de ComfyUI",
|
||||
"Contact Support": "Contactar soporte",
|
||||
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
|
||||
"Custom Nodes Manager": "Gestor de nodos personalizados",
|
||||
"Delete Selected Items": "Eliminar elementos seleccionados",
|
||||
@@ -603,6 +649,7 @@
|
||||
"Open Logs Folder": "Abrir carpeta de registros",
|
||||
"Open Models Folder": "Abrir carpeta de modelos",
|
||||
"Open Outputs Folder": "Abrir carpeta de salidas",
|
||||
"Open Sign In Dialog": "Abrir diálogo de inicio de sesión",
|
||||
"Open extra_model_paths_yaml": "Abrir extra_model_paths.yaml",
|
||||
"Pin/Unpin Selected Items": "Anclar/Desanclar elementos seleccionados",
|
||||
"Pin/Unpin Selected Nodes": "Anclar/Desanclar nodos seleccionados",
|
||||
@@ -619,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",
|
||||
@@ -872,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",
|
||||
@@ -880,6 +929,7 @@
|
||||
"Graph": "Gráfico",
|
||||
"Group": "Grupo",
|
||||
"Keybinding": "Asignación de Teclas",
|
||||
"Light": "Claro",
|
||||
"Link": "Enlace",
|
||||
"LinkRelease": "Liberación de Enlace",
|
||||
"LiteGraph": "Lite Graph",
|
||||
@@ -905,6 +955,7 @@
|
||||
"Sidebar": "Barra Lateral",
|
||||
"Tree Explorer": "Explorador de Árbol",
|
||||
"UV": "UV",
|
||||
"User": "Usuario",
|
||||
"Validation": "Validación",
|
||||
"Window": "Ventana",
|
||||
"Workflow": "Flujo de Trabajo"
|
||||
@@ -1059,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",
|
||||
@@ -1075,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",
|
||||
@@ -1084,6 +1142,17 @@
|
||||
"next": "Siguiente",
|
||||
"selectUser": "Selecciona un usuario"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "Correo electrónico",
|
||||
"name": "Nombre",
|
||||
"notSet": "No establecido",
|
||||
"provider": "Método de inicio de sesión",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "Configuración de usuario"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Dirección de correo electrónico inválida",
|
||||
"maxLength": "No debe tener más de {length} caracteres",
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
"Straight": "Recto"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Color de fondo inicial",
|
||||
"tooltip": "Controla el color de fondo predeterminado de la escena 3D. Esta configuración determina la apariencia del fondo cuando se crea un nuevo widget 3D, pero puede ajustarse individualmente para cada widget después de su creación."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Tipo de Cámara",
|
||||
"options": {
|
||||
@@ -116,6 +120,22 @@
|
||||
},
|
||||
"tooltip": "Controla si la cámara es perspectiva u ortográfica por defecto cuando se crea un nuevo widget 3D. Este valor predeterminado aún puede ser alternado individualmente para cada widget después de su creación."
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "Incremento de ajuste de luz",
|
||||
"tooltip": "Controla el tamaño del incremento al ajustar la intensidad de la luz en escenas 3D. Un valor de paso más pequeño permite un control más preciso sobre los ajustes de iluminación, mientras que un valor más grande resulta en cambios más notorios por cada ajuste."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "Intensidad Inicial de la Luz",
|
||||
"tooltip": "Establece el nivel de brillo predeterminado de la iluminación en la escena 3D. Este valor determina cuán intensamente las luces iluminan los objetos cuando se crea un nuevo widget 3D, pero puede ajustarse individualmente para cada widget después de la creación."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "Intensidad Máxima de Luz",
|
||||
"tooltip": "Establece el valor máximo permitido de intensidad de luz para escenas 3D. Esto define el límite superior de brillo que se puede ajustar al modificar la iluminación en cualquier widget 3D."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "Intensidad de luz mínima",
|
||||
"tooltip": "Establece el valor mínimo permitido de intensidad de luz para escenas 3D. Esto define el límite inferior de brillo que se puede ajustar al modificar la iluminación en cualquier widget 3D."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Mostrar Cuadrícula",
|
||||
"tooltip": "Cambiar para mostrar cuadrícula por defecto"
|
||||
@@ -273,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."
|
||||
@@ -357,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"
|
||||
},
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "Effacer le flux de travail"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Contacter le support"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Dupliquer le flux de travail actuel"
|
||||
},
|
||||
@@ -173,6 +176,12 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Annuler"
|
||||
},
|
||||
"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,6 +17,7 @@
|
||||
"emailPlaceholder": "Entrez votre email",
|
||||
"failed": "Échec de la connexion",
|
||||
"forgotPassword": "Mot de passe oublié?",
|
||||
"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",
|
||||
@@ -24,13 +25,21 @@
|
||||
"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",
|
||||
"success": "Connexion réussie",
|
||||
"termsLink": "Conditions d'utilisation",
|
||||
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
|
||||
"title": "Connectez-vous à votre compte"
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Se déconnecter",
|
||||
"success": "Déconnexion réussie",
|
||||
"successDetail": "Vous avez été déconnecté de votre compte."
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "Vous avez déjà un compte?",
|
||||
"emailLabel": "Email",
|
||||
@@ -92,6 +101,24 @@
|
||||
"Title": "Titre",
|
||||
"Unpin": "Désépingler"
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Crédits",
|
||||
"faqs": "FAQ",
|
||||
"invoiceHistory": "Historique des factures",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"messageSupport": "Contacter le support",
|
||||
"purchaseCredits": "Acheter des crédits",
|
||||
"topUp": {
|
||||
"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)",
|
||||
"quickPurchase": "Achat rapide",
|
||||
"seeDetails": "Voir les détails",
|
||||
"topUp": "Recharger"
|
||||
},
|
||||
"yourCreditBalance": "Votre solde de crédits"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "AUDIO",
|
||||
"BOOLEAN": "BOOLEAN",
|
||||
@@ -168,6 +195,7 @@
|
||||
"about": "À propos",
|
||||
"add": "Ajouter",
|
||||
"all": "Tout",
|
||||
"amount": "Quantité",
|
||||
"apply": "Appliquer",
|
||||
"back": "Retour",
|
||||
"cancel": "Annuler",
|
||||
@@ -269,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"
|
||||
},
|
||||
@@ -392,19 +422,34 @@
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Contactez-moi pour un suivi",
|
||||
"contactSupportDescription": "Veuillez remplir le formulaire ci-dessous avec votre signalement",
|
||||
"contactSupportTitle": "Contacter le support",
|
||||
"describeTheProblem": "Décrivez le problème",
|
||||
"email": "E-mail",
|
||||
"feedbackTitle": "Aidez-nous à améliorer ComfyUI en fournissant des commentaires",
|
||||
"helpFix": "Aidez à résoudre cela",
|
||||
"helpTypes": {
|
||||
"billingPayments": "Facturation / Paiements",
|
||||
"bugReport": "Signaler un bug",
|
||||
"giveFeedback": "Donner un avis",
|
||||
"loginAccessIssues": "Problèmes de connexion / d'accès",
|
||||
"somethingElse": "Autre chose"
|
||||
},
|
||||
"notifyResolve": "Prévenez-moi lorsque résolu",
|
||||
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
|
||||
"provideEmail": "Donnez-nous votre email (Facultatif)",
|
||||
"rating": "Évaluation",
|
||||
"selectIssue": "Sélectionnez le problème",
|
||||
"stackTrace": "Trace de la pile",
|
||||
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
|
||||
"systemStats": "Statistiques du système",
|
||||
"validation": {
|
||||
"invalidEmail": "Veuillez entrer une adresse e-mail valide",
|
||||
"maxLength": "Message trop long"
|
||||
}
|
||||
"maxLength": "Message trop long",
|
||||
"selectIssueType": "Veuillez sélectionner un type de problème"
|
||||
},
|
||||
"whatCanWeInclude": "Précisez ce qu'il faut inclure dans le rapport",
|
||||
"whatDoYouNeedHelpWith": "Avec quoi avez-vous besoin d'aide ?"
|
||||
},
|
||||
"load3d": {
|
||||
"applyingTexture": "Application de la texture...",
|
||||
@@ -577,6 +622,7 @@
|
||||
"ComfyUI Docs": "Docs de ComfyUI",
|
||||
"ComfyUI Forum": "Forum ComfyUI",
|
||||
"ComfyUI Issues": "Problèmes de ComfyUI",
|
||||
"Contact Support": "Contacter le support",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
|
||||
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
||||
@@ -603,6 +649,7 @@
|
||||
"Open Logs Folder": "Ouvrir le dossier des journaux",
|
||||
"Open Models Folder": "Ouvrir le dossier des modèles",
|
||||
"Open Outputs Folder": "Ouvrir le dossier des sorties",
|
||||
"Open Sign In Dialog": "Ouvrir la boîte de dialogue de connexion",
|
||||
"Open extra_model_paths_yaml": "Ouvrir extra_model_paths.yaml",
|
||||
"Pin/Unpin Selected Items": "Épingler/Désépingler les éléments sélectionnés",
|
||||
"Pin/Unpin Selected Nodes": "Épingler/Désépingler les nœuds sélectionnés",
|
||||
@@ -619,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",
|
||||
@@ -872,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",
|
||||
@@ -880,6 +929,7 @@
|
||||
"Graph": "Graphique",
|
||||
"Group": "Groupe",
|
||||
"Keybinding": "Raccourci Clavier",
|
||||
"Light": "Clair",
|
||||
"Link": "Lien",
|
||||
"LinkRelease": "Libération de Lien",
|
||||
"LiteGraph": "Lite Graph",
|
||||
@@ -905,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"
|
||||
@@ -1059,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",
|
||||
@@ -1075,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",
|
||||
@@ -1084,6 +1142,17 @@
|
||||
"next": "Suivant",
|
||||
"selectUser": "Sélectionnez un utilisateur"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "E-mail",
|
||||
"name": "Nom",
|
||||
"notSet": "Non défini",
|
||||
"provider": "Méthode de connexion",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "Paramètres utilisateur"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Adresse e-mail invalide",
|
||||
"maxLength": "Ne doit pas dépasser {length} caractères",
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
"Straight": "Droit"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Couleur de fond initiale",
|
||||
"tooltip": "Contrôle la couleur de fond par défaut de la scène 3D. Ce paramètre détermine l'apparence du fond lors de la création d'un nouveau widget 3D, mais peut être ajusté individuellement pour chaque widget après la création."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Type de Caméra",
|
||||
"options": {
|
||||
@@ -116,6 +120,22 @@
|
||||
},
|
||||
"tooltip": "Contrôle si la caméra est en perspective ou orthographique par défaut lorsqu'un nouveau widget 3D est créé. Ce défaut peut toujours être basculé individuellement pour chaque widget après sa création."
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "Incrément d'ajustement de la lumière",
|
||||
"tooltip": "Contrôle la taille de l'incrément lors de l'ajustement de l'intensité lumineuse dans les scènes 3D. Une valeur de pas plus petite permet un contrôle plus précis des ajustements de lumière, tandis qu'une valeur plus grande entraîne des changements plus visibles à chaque ajustement."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "Intensité lumineuse initiale",
|
||||
"tooltip": "Définit le niveau de luminosité par défaut de l’éclairage dans la scène 3D. Cette valeur détermine l’intensité avec laquelle les lumières illuminent les objets lors de la création d’un nouveau widget 3D, mais peut être ajustée individuellement pour chaque widget après la création."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "Intensité lumineuse maximale",
|
||||
"tooltip": "Définit la valeur maximale autorisée pour l’intensité lumineuse dans les scènes 3D. Cela fixe la limite supérieure de luminosité pouvant être réglée lors de l’ajustement de l’éclairage dans tout widget 3D."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "Intensité lumineuse minimale",
|
||||
"tooltip": "Définit la valeur minimale autorisée de l’intensité lumineuse pour les scènes 3D. Cela définit la limite inférieure de luminosité pouvant être réglée lors de l’ajustement de l’éclairage dans tout widget 3D."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Afficher la Grille",
|
||||
"tooltip": "Basculer pour afficher la grille par défaut"
|
||||
@@ -273,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."
|
||||
@@ -357,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"
|
||||
},
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "ワークフローをクリア"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "サポートに連絡"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "現在のワークフローを複製"
|
||||
},
|
||||
@@ -173,6 +176,12 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "元に戻す"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "サインインダイアログを開く"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "サインアウト"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "現在のワークフローを閉じる"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "メールアドレスを入力してください",
|
||||
"failed": "ログイン失敗",
|
||||
"forgotPassword": "パスワードを忘れましたか?",
|
||||
"forgotPasswordError": "パスワードリセット用メールの送信に失敗しました",
|
||||
"loginButton": "ログイン",
|
||||
"loginWithGithub": "Githubでログイン",
|
||||
"loginWithGoogle": "Googleでログイン",
|
||||
@@ -24,13 +25,21 @@
|
||||
"orContinueWith": "または以下で続ける",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholder": "パスワードを入力してください",
|
||||
"passwordResetSent": "パスワードリセット用メールを送信しました",
|
||||
"passwordResetSentDetail": "パスワードをリセットするためのリンクが記載されたメールをご確認ください。",
|
||||
"privacyLink": "プライバシーポリシー",
|
||||
"signInOrSignUp": "サインイン / サインアップ",
|
||||
"signUp": "サインアップ",
|
||||
"success": "ログイン成功",
|
||||
"termsLink": "利用規約",
|
||||
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
|
||||
"title": "アカウントにログインする"
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "ログアウト",
|
||||
"success": "正常にサインアウトしました",
|
||||
"successDetail": "アカウントからサインアウトしました。"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "すでにアカウントをお持ちですか?",
|
||||
"emailLabel": "メール",
|
||||
@@ -92,6 +101,24 @@
|
||||
"Title": "タイトル",
|
||||
"Unpin": "ピンを解除"
|
||||
},
|
||||
"credits": {
|
||||
"credits": "クレジット",
|
||||
"faqs": "よくある質問",
|
||||
"invoiceHistory": "請求履歴",
|
||||
"lastUpdated": "最終更新",
|
||||
"messageSupport": "サポートにメッセージ",
|
||||
"purchaseCredits": "クレジットを購入",
|
||||
"topUp": {
|
||||
"buyNow": "今すぐ購入",
|
||||
"insufficientMessage": "このワークフローを実行するのに十分なクレジットがありません。",
|
||||
"insufficientTitle": "クレジット不足",
|
||||
"maxAmount": "(最大 $1,000 USD)",
|
||||
"quickPurchase": "クイック購入",
|
||||
"seeDetails": "詳細を見る",
|
||||
"topUp": "チャージ"
|
||||
},
|
||||
"yourCreditBalance": "あなたのクレジット残高"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "オーディオ",
|
||||
"BOOLEAN": "ブール",
|
||||
@@ -168,6 +195,7 @@
|
||||
"about": "情報",
|
||||
"add": "追加",
|
||||
"all": "すべて",
|
||||
"amount": "量",
|
||||
"apply": "適用する",
|
||||
"back": "戻る",
|
||||
"cancel": "キャンセル",
|
||||
@@ -269,11 +297,13 @@
|
||||
"success": "成功",
|
||||
"systemInfo": "システム情報",
|
||||
"terminal": "ターミナル",
|
||||
"unknownError": "不明なエラー",
|
||||
"update": "更新",
|
||||
"updateAvailable": "更新が利用可能",
|
||||
"updated": "更新済み",
|
||||
"updating": "更新中",
|
||||
"upload": "アップロード",
|
||||
"user": "ユーザー",
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
|
||||
"workflow": "ワークフロー"
|
||||
},
|
||||
@@ -392,19 +422,34 @@
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "フォローアップのために私に連絡する",
|
||||
"contactSupportDescription": "下記のフォームにご報告内容をご記入ください",
|
||||
"contactSupportTitle": "サポートに連絡",
|
||||
"describeTheProblem": "問題の内容を記述してください",
|
||||
"email": "メールアドレス",
|
||||
"feedbackTitle": "フィードバックを提供してComfyUIの改善にご協力ください",
|
||||
"helpFix": "これを修正するのを助ける",
|
||||
"helpTypes": {
|
||||
"billingPayments": "請求/支払い",
|
||||
"bugReport": "バグ報告",
|
||||
"giveFeedback": "フィードバックを送る",
|
||||
"loginAccessIssues": "ログイン/アクセスの問題",
|
||||
"somethingElse": "その他"
|
||||
},
|
||||
"notifyResolve": "解決したときに通知する",
|
||||
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
|
||||
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
|
||||
"rating": "評価",
|
||||
"selectIssue": "問題を選択してください",
|
||||
"stackTrace": "スタックトレース",
|
||||
"submitErrorReport": "エラーレポートを提出する(オプション)",
|
||||
"systemStats": "システム統計",
|
||||
"validation": {
|
||||
"invalidEmail": "有効なメールアドレスを入力してください",
|
||||
"maxLength": "メッセージが長すぎます"
|
||||
}
|
||||
"maxLength": "メッセージが長すぎます",
|
||||
"selectIssueType": "問題の種類を選択してください"
|
||||
},
|
||||
"whatCanWeInclude": "レポートに含める内容を指定してください",
|
||||
"whatDoYouNeedHelpWith": "どのようなサポートが必要ですか?"
|
||||
},
|
||||
"load3d": {
|
||||
"applyingTexture": "テクスチャを適用中...",
|
||||
@@ -577,6 +622,7 @@
|
||||
"ComfyUI Docs": "ComfyUIのドキュメント",
|
||||
"ComfyUI Forum": "ComfyUI フォーラム",
|
||||
"ComfyUI Issues": "ComfyUIの問題",
|
||||
"Contact Support": "サポートに連絡",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Custom Nodes Manager": "カスタムノードマネージャ",
|
||||
"Delete Selected Items": "選択したアイテムを削除",
|
||||
@@ -603,6 +649,7 @@
|
||||
"Open Logs Folder": "ログフォルダを開く",
|
||||
"Open Models Folder": "モデルフォルダを開く",
|
||||
"Open Outputs Folder": "出力フォルダを開く",
|
||||
"Open Sign In Dialog": "サインインダイアログを開く",
|
||||
"Open extra_model_paths_yaml": "extra_model_paths.yamlを開く",
|
||||
"Pin/Unpin Selected Items": "選択したアイテムのピン留め/ピン留め解除",
|
||||
"Pin/Unpin Selected Nodes": "選択したノードのピン留め/ピン留め解除",
|
||||
@@ -619,6 +666,7 @@
|
||||
"Save": "保存",
|
||||
"Save As": "名前を付けて保存",
|
||||
"Show Settings Dialog": "設定ダイアログを表示",
|
||||
"Sign Out": "サインアウト",
|
||||
"Toggle Bottom Panel": "下部パネルの切り替え",
|
||||
"Toggle Focus Mode": "フォーカスモードの切り替え",
|
||||
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
|
||||
@@ -872,6 +920,7 @@
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfyデスクトップ",
|
||||
"ContextMenu": "コンテキストメニュー",
|
||||
"Credits": "クレジット",
|
||||
"CustomColorPalettes": "カスタムカラーパレット",
|
||||
"DevMode": "開発モード",
|
||||
"EditTokenWeight": "トークンの重みを編集",
|
||||
@@ -880,6 +929,7 @@
|
||||
"Graph": "グラフ",
|
||||
"Group": "グループ",
|
||||
"Keybinding": "キー割り当て",
|
||||
"Light": "ライト",
|
||||
"Link": "リンク",
|
||||
"LinkRelease": "リンク解除",
|
||||
"LiteGraph": "Lite Graph",
|
||||
@@ -905,6 +955,7 @@
|
||||
"Sidebar": "サイドバー",
|
||||
"Tree Explorer": "ツリーエクスプローラー",
|
||||
"UV": "UV",
|
||||
"User": "ユーザー",
|
||||
"Validation": "検証",
|
||||
"Window": "ウィンドウ",
|
||||
"Workflow": "ワークフロー"
|
||||
@@ -1059,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シーンがありません",
|
||||
@@ -1075,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "保留中のタスクが削除されました",
|
||||
"pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください",
|
||||
"unableToGetModelFilePath": "モデルファイルのパスを取得できません",
|
||||
"updateRequested": "更新が要求されました"
|
||||
"updateRequested": "更新が要求されました",
|
||||
"userNotAuthenticated": "ユーザーが認証されていません"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "ユーザー名を入力してください",
|
||||
@@ -1084,6 +1142,17 @@
|
||||
"next": "次へ",
|
||||
"selectUser": "ユーザーを選択"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "メールアドレス",
|
||||
"name": "名前",
|
||||
"notSet": "未設定",
|
||||
"provider": "サインイン方法",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "ユーザー設定"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "無効なメールアドレス",
|
||||
"maxLength": "{length}文字以下でなければなりません",
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
"Straight": "ストレート"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "初期背景色",
|
||||
"tooltip": "3Dシーンのデフォルト背景色を設定します。この設定は新しい3Dウィジェット作成時の背景の見た目を決定しますが、作成後に各ウィジェットごとに個別に調整できます。"
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "カメラタイプ",
|
||||
"options": {
|
||||
@@ -116,6 +120,22 @@
|
||||
},
|
||||
"tooltip": "新しい3Dウィジェットが作成されたときに、デフォルトでカメラが透視投影か平行投影かを制御します。このデフォルトは、作成後に各ウィジェットごとに個別に切り替えることができます。"
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "ライト調整増分",
|
||||
"tooltip": "3Dシーンでライトの強度を調整する際の増分サイズを制御します。ステップ値が小さいほど、照明調整をより細かく制御でき、大きい値では1回の調整ごとにより顕著な変化が得られます。"
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "初期ライト強度",
|
||||
"tooltip": "3Dシーン内の照明のデフォルトの明るさレベルを設定します。この値は新しい3Dウィジェット作成時にライトがオブジェクトをどれだけ強く照らすかを決定しますが、作成後に各ウィジェットごとに個別に調整できます。"
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "最大光度",
|
||||
"tooltip": "3Dシーンで許可される最大光度値を設定します。これは、3Dウィジェットで照明を調整する際に設定できる明るさの上限を定義します。"
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "光の強度の最小値",
|
||||
"tooltip": "3Dシーンで許可される光の強度の最小値を設定します。これは、3Dウィジェットで照明を調整する際に設定できる明るさの下限を定義します。"
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "グリッドを表示",
|
||||
"tooltip": "デフォルトでグリッドを表示するには切り替えます"
|
||||
@@ -273,6 +293,9 @@
|
||||
"small": "小"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "サイドバーの幅を統一"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "グリッドサイズにスナップ",
|
||||
"tooltip": "シフトを押しながらノードをドラッグおよびサイズ変更すると、グリッドに整列されます。これにより、そのグリッドのサイズが制御されます。"
|
||||
@@ -357,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "ズームイン時にノードコンボウィジェットメニュー(リスト)をスケーリングする"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "新しいノードを常に縮小",
|
||||
"tooltip": "ノード作成時に可能な限り小さいサイズにリサイズします。無効にすると、新しく追加されたノードはウィジェットの値が表示されるように少し幅広くなります。"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "ツールチップ遅延"
|
||||
},
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "워크플로 지우기"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "지원팀에 문의하기"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "현재 워크플로우 복제"
|
||||
},
|
||||
@@ -173,6 +176,12 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "실행 취소"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "로그인 대화상자 열기"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "로그아웃"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "현재 워크플로우 닫기"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "이메일을 입력하세요",
|
||||
"failed": "로그인 실패",
|
||||
"forgotPassword": "비밀번호를 잊으셨나요?",
|
||||
"forgotPasswordError": "비밀번호 재설정 이메일 전송에 실패했습니다",
|
||||
"loginButton": "로그인",
|
||||
"loginWithGithub": "Github로 로그인",
|
||||
"loginWithGoogle": "구글로 로그인",
|
||||
@@ -24,13 +25,21 @@
|
||||
"orContinueWith": "또는 다음으로 계속",
|
||||
"passwordLabel": "비밀번호",
|
||||
"passwordPlaceholder": "비밀번호를 입력하세요",
|
||||
"passwordResetSent": "비밀번호 재설정 이메일이 전송되었습니다",
|
||||
"passwordResetSentDetail": "비밀번호를 재설정할 수 있는 링크가 포함된 이메일을 확인해 주세요.",
|
||||
"privacyLink": "개인정보 보호정책",
|
||||
"signInOrSignUp": "로그인 / 회원가입",
|
||||
"signUp": "가입하기",
|
||||
"success": "로그인 성공",
|
||||
"termsLink": "이용 약관",
|
||||
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
|
||||
"title": "계정에 로그인"
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "로그아웃",
|
||||
"success": "성공적으로 로그아웃되었습니다",
|
||||
"successDetail": "계정에서 로그아웃되었습니다."
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "이미 계정이 있으신가요?",
|
||||
"emailLabel": "이메일",
|
||||
@@ -92,6 +101,24 @@
|
||||
"Title": "제목",
|
||||
"Unpin": "고정 해제"
|
||||
},
|
||||
"credits": {
|
||||
"credits": "크레딧",
|
||||
"faqs": "자주 묻는 질문",
|
||||
"invoiceHistory": "청구서 내역",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"messageSupport": "지원 문의",
|
||||
"purchaseCredits": "크레딧 구매",
|
||||
"topUp": {
|
||||
"buyNow": "지금 구매",
|
||||
"insufficientMessage": "이 워크플로우를 실행하기에 크레딧이 부족합니다.",
|
||||
"insufficientTitle": "크레딧 부족",
|
||||
"maxAmount": "(최대 $1,000 USD)",
|
||||
"quickPurchase": "빠른 구매",
|
||||
"seeDetails": "자세히 보기",
|
||||
"topUp": "충전하기"
|
||||
},
|
||||
"yourCreditBalance": "보유 크레딧 잔액"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "오디오",
|
||||
"BOOLEAN": "논리값",
|
||||
@@ -168,6 +195,7 @@
|
||||
"about": "정보",
|
||||
"add": "추가",
|
||||
"all": "모두",
|
||||
"amount": "수량",
|
||||
"apply": "적용",
|
||||
"back": "뒤로",
|
||||
"cancel": "취소",
|
||||
@@ -269,11 +297,13 @@
|
||||
"success": "성공",
|
||||
"systemInfo": "시스템 정보",
|
||||
"terminal": "터미널",
|
||||
"unknownError": "알 수 없는 오류",
|
||||
"update": "업데이트",
|
||||
"updateAvailable": "업데이트 가능",
|
||||
"updated": "업데이트 됨",
|
||||
"updating": "업데이트 중",
|
||||
"upload": "업로드",
|
||||
"user": "사용자",
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
|
||||
"workflow": "워크플로"
|
||||
},
|
||||
@@ -392,19 +422,34 @@
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "추적 조사를 위해 연락해 주세요",
|
||||
"contactSupportDescription": "아래 양식에 보고 내용을 작성해 주세요",
|
||||
"contactSupportTitle": "지원팀에 문의하기",
|
||||
"describeTheProblem": "문제를 설명해 주세요",
|
||||
"email": "이메일",
|
||||
"feedbackTitle": "피드백을 제공함으로써 ComfyUI를 개선하는 데 도움을 주십시오",
|
||||
"helpFix": "이 문제 해결에 도움을 주세요",
|
||||
"helpTypes": {
|
||||
"billingPayments": "결제 / 지불",
|
||||
"bugReport": "버그 신고",
|
||||
"giveFeedback": "피드백 제공",
|
||||
"loginAccessIssues": "로그인 / 접근 문제",
|
||||
"somethingElse": "기타"
|
||||
},
|
||||
"notifyResolve": "해결되었을 때 알려주세요",
|
||||
"provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
|
||||
"provideEmail": "이메일을 알려주세요 (선택 사항)",
|
||||
"rating": "평가",
|
||||
"selectIssue": "문제를 선택하세요",
|
||||
"stackTrace": "스택 추적",
|
||||
"submitErrorReport": "오류 보고서 제출 (선택 사항)",
|
||||
"systemStats": "시스템 통계",
|
||||
"validation": {
|
||||
"invalidEmail": "유효한 이메일 주소를 입력해 주세요",
|
||||
"maxLength": "메시지가 너무 깁니다"
|
||||
}
|
||||
"maxLength": "메시지가 너무 깁니다",
|
||||
"selectIssueType": "문제 유형을 선택해 주세요"
|
||||
},
|
||||
"whatCanWeInclude": "보고서에 포함할 내용을 지정하세요",
|
||||
"whatDoYouNeedHelpWith": "어떤 도움이 필요하신가요?"
|
||||
},
|
||||
"load3d": {
|
||||
"applyingTexture": "텍스처 적용 중...",
|
||||
@@ -577,6 +622,7 @@
|
||||
"ComfyUI Docs": "ComfyUI 문서",
|
||||
"ComfyUI Forum": "ComfyUI 포럼",
|
||||
"ComfyUI Issues": "ComfyUI 이슈 페이지",
|
||||
"Contact Support": "고객 지원 문의",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Custom Nodes Manager": "사용자 정의 노드 관리자",
|
||||
"Delete Selected Items": "선택한 항목 삭제",
|
||||
@@ -603,6 +649,7 @@
|
||||
"Open Logs Folder": "로그 폴더 열기",
|
||||
"Open Models Folder": "모델 폴더 열기",
|
||||
"Open Outputs Folder": "출력 폴더 열기",
|
||||
"Open Sign In Dialog": "로그인 대화 상자 열기",
|
||||
"Open extra_model_paths_yaml": "extra_model_paths.yaml 열기",
|
||||
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
|
||||
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
|
||||
@@ -619,6 +666,7 @@
|
||||
"Save": "저장",
|
||||
"Save As": "다른 이름으로 저장",
|
||||
"Show Settings Dialog": "설정 대화상자 표시",
|
||||
"Sign Out": "로그아웃",
|
||||
"Toggle Bottom Panel": "하단 패널 전환",
|
||||
"Toggle Focus Mode": "포커스 모드 전환",
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
@@ -872,6 +920,7 @@
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfy-Desktop",
|
||||
"ContextMenu": "컨텍스트 메뉴",
|
||||
"Credits": "크레딧",
|
||||
"CustomColorPalettes": "사용자 정의 색상 팔레트",
|
||||
"DevMode": "개발자 모드",
|
||||
"EditTokenWeight": "토큰 가중치 편집",
|
||||
@@ -880,6 +929,7 @@
|
||||
"Graph": "그래프",
|
||||
"Group": "그룹",
|
||||
"Keybinding": "키 바인딩",
|
||||
"Light": "라이트",
|
||||
"Link": "링크",
|
||||
"LinkRelease": "링크 해제",
|
||||
"LiteGraph": "LiteGraph",
|
||||
@@ -905,6 +955,7 @@
|
||||
"Sidebar": "사이드바",
|
||||
"Tree Explorer": "트리 탐색기",
|
||||
"UV": "UV",
|
||||
"User": "사용자",
|
||||
"Validation": "검증",
|
||||
"Window": "창",
|
||||
"Workflow": "워크플로"
|
||||
@@ -1059,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 장면이 없습니다",
|
||||
@@ -1075,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",
|
||||
"pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요",
|
||||
"unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다",
|
||||
"updateRequested": "업데이트 요청됨"
|
||||
"updateRequested": "업데이트 요청됨",
|
||||
"userNotAuthenticated": "사용자가 인증되지 않았습니다"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "사용자 이름 입력",
|
||||
@@ -1084,6 +1142,17 @@
|
||||
"next": "다음",
|
||||
"selectUser": "사용자 선택"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "이메일",
|
||||
"name": "이름",
|
||||
"notSet": "설정되지 않음",
|
||||
"provider": "로그인 방법",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "사용자 설정"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "유효하지 않은 이메일 주소",
|
||||
"maxLength": "{length}자를 초과할 수 없습니다",
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
"Straight": "직선"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "초기 배경색",
|
||||
"tooltip": "3D 장면의 기본 배경색을 설정합니다. 이 설정은 새 3D 위젯이 생성될 때 배경의 모양을 결정하지만, 생성 후 각 위젯별로 개별적으로 조정할 수 있습니다."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "카메라 유형",
|
||||
"options": {
|
||||
@@ -116,6 +120,22 @@
|
||||
},
|
||||
"tooltip": "새로운 3D 위젯이 생성될 때 카메라가 기본적으로 원근법 또는 직교법을 사용하는지를 제어합니다. 이 기본값은 생성 후 각 위젯별로 개별적으로 전환할 수 있습니다."
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "조명 조정 증가량",
|
||||
"tooltip": "3D 장면에서 조명 강도를 조정할 때 증가하는 크기를 제어합니다. 값이 작을수록 조명을 더 세밀하게 조정할 수 있고, 값이 클수록 한 번에 더 눈에 띄는 변화가 발생합니다."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "초기 조명 강도",
|
||||
"tooltip": "3D 장면에서 조명의 기본 밝기 수준을 설정합니다. 이 값은 새 3D 위젯이 생성될 때 조명이 오브젝트를 얼마나 강하게 비추는지 결정하지만, 생성 후 각 위젯별로 개별적으로 조정할 수 있습니다."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "최대 조명 강도",
|
||||
"tooltip": "3D 장면에서 허용되는 최대 조명 강도 값을 설정합니다. 이는 모든 3D 위젯에서 조명을 조정할 때 설정할 수 있는 밝기의 상한선을 정의합니다."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "최소 광원 세기",
|
||||
"tooltip": "3D 장면에서 허용되는 최소 광원 세기 값을 설정합니다. 이는 모든 3D 위젯에서 조명을 조정할 때 설정할 수 있는 밝기의 하한을 정의합니다."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "그리드 표시",
|
||||
"tooltip": "기본적으로 그리드를 표시하도록 전환"
|
||||
@@ -273,6 +293,9 @@
|
||||
"small": "작음"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "통합 사이드바 너비"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "그리드 크기에 스냅",
|
||||
"tooltip": "Shift 키를 누른 상태에서 노드를 드래그하거나 크기를 조절하면 그리드에 맞춰 정렬됩니다. 이 설정은 그리드의 크기를 제어합니다."
|
||||
@@ -357,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "확대시 노드 콤보 위젯 메뉴 (목록) 스케일링"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "새 노드를 항상 축소",
|
||||
"tooltip": "노드를 생성할 때 가능한 가장 작은 크기로 크기를 조정합니다. 비활성화하면 새로 추가된 노드가 위젯 값을 표시하기 위해 약간 넓어집니다."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "툴팁 지연"
|
||||
},
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "Очистить рабочий процесс"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "Связаться с поддержкой"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "Дублировать текущий рабочий процесс"
|
||||
},
|
||||
@@ -173,6 +176,12 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "Отменить"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Открыть окно входа"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Выйти"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Закрыть текущий рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "Введите вашу электронную почту",
|
||||
"failed": "Вход не удался",
|
||||
"forgotPassword": "Забыли пароль?",
|
||||
"forgotPasswordError": "Не удалось отправить письмо для сброса пароля",
|
||||
"loginButton": "Войти",
|
||||
"loginWithGithub": "Войти через Github",
|
||||
"loginWithGoogle": "Войти через Google",
|
||||
@@ -24,13 +25,21 @@
|
||||
"orContinueWith": "Или продолжить с",
|
||||
"passwordLabel": "Пароль",
|
||||
"passwordPlaceholder": "Введите ваш пароль",
|
||||
"passwordResetSent": "Письмо для сброса пароля отправлено",
|
||||
"passwordResetSentDetail": "Пожалуйста, проверьте свою электронную почту для получения ссылки на сброс пароля.",
|
||||
"privacyLink": "Политикой конфиденциальности",
|
||||
"signInOrSignUp": "Войти / Зарегистрироваться",
|
||||
"signUp": "Зарегистрироваться",
|
||||
"success": "Вход выполнен успешно",
|
||||
"termsLink": "Условиями использования",
|
||||
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
|
||||
"title": "Войдите в свой аккаунт"
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Выйти",
|
||||
"success": "Вы успешно вышли из системы",
|
||||
"successDetail": "Вы вышли из своей учетной записи."
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "Уже есть аккаунт?",
|
||||
"emailLabel": "Электронная почта",
|
||||
@@ -92,6 +101,24 @@
|
||||
"Title": "Заголовок",
|
||||
"Unpin": "Открепить"
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Кредиты",
|
||||
"faqs": "Часто задаваемые вопросы",
|
||||
"invoiceHistory": "История счетов",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"messageSupport": "Связаться с поддержкой",
|
||||
"purchaseCredits": "Купить кредиты",
|
||||
"topUp": {
|
||||
"buyNow": "Купить сейчас",
|
||||
"insufficientMessage": "У вас недостаточно кредитов для запуска этого рабочего процесса.",
|
||||
"insufficientTitle": "Недостаточно кредитов",
|
||||
"maxAmount": "(Макс. $1,000 USD)",
|
||||
"quickPurchase": "Быстрая покупка",
|
||||
"seeDetails": "Смотреть детали",
|
||||
"topUp": "Пополнить"
|
||||
},
|
||||
"yourCreditBalance": "Ваш баланс кредитов"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "АУДИО",
|
||||
"BOOLEAN": "БУЛЕВО",
|
||||
@@ -168,6 +195,7 @@
|
||||
"about": "О программе",
|
||||
"add": "Добавить",
|
||||
"all": "Все",
|
||||
"amount": "Количество",
|
||||
"apply": "Применить",
|
||||
"back": "Назад",
|
||||
"cancel": "Отмена",
|
||||
@@ -269,11 +297,13 @@
|
||||
"success": "Успех",
|
||||
"systemInfo": "Информация о системе",
|
||||
"terminal": "Терминал",
|
||||
"unknownError": "Неизвестная ошибка",
|
||||
"update": "Обновить",
|
||||
"updateAvailable": "Доступно обновление",
|
||||
"updated": "Обновлено",
|
||||
"updating": "Обновление",
|
||||
"upload": "Загрузить",
|
||||
"user": "Пользователь",
|
||||
"videoFailedToLoad": "Не удалось загрузить видео",
|
||||
"workflow": "Рабочий процесс"
|
||||
},
|
||||
@@ -392,19 +422,34 @@
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Свяжитесь со мной для уточнения",
|
||||
"contactSupportDescription": "Пожалуйста, заполните форму ниже для отправки вашего отчёта",
|
||||
"contactSupportTitle": "Связаться с поддержкой",
|
||||
"describeTheProblem": "Опишите проблему",
|
||||
"email": "Электронная почта",
|
||||
"feedbackTitle": "Помогите нам улучшить ComfyUI, оставив отзыв",
|
||||
"helpFix": "Помочь исправить это",
|
||||
"helpTypes": {
|
||||
"billingPayments": "Оплата / Платежи",
|
||||
"bugReport": "Сообщить об ошибке",
|
||||
"giveFeedback": "Оставить отзыв",
|
||||
"loginAccessIssues": "Проблемы со входом / доступом",
|
||||
"somethingElse": "Другое"
|
||||
},
|
||||
"notifyResolve": "Уведомить меня, когда проблема будет решена",
|
||||
"provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
|
||||
"provideEmail": "Укажите вашу электронную почту (необязательно)",
|
||||
"rating": "Рейтинг",
|
||||
"selectIssue": "Выберите проблему",
|
||||
"stackTrace": "Трассировка стека",
|
||||
"submitErrorReport": "Отправить отчёт об ошибке (необязательно)",
|
||||
"systemStats": "Статистика системы",
|
||||
"validation": {
|
||||
"invalidEmail": "Пожалуйста, введите действительный адрес электронной почты",
|
||||
"maxLength": "Сообщение слишком длинное"
|
||||
}
|
||||
"maxLength": "Сообщение слишком длинное",
|
||||
"selectIssueType": "Пожалуйста, выберите тип проблемы"
|
||||
},
|
||||
"whatCanWeInclude": "Уточните, что включить в отчёт",
|
||||
"whatDoYouNeedHelpWith": "С чем вам нужна помощь?"
|
||||
},
|
||||
"load3d": {
|
||||
"applyingTexture": "Применение текстуры...",
|
||||
@@ -577,6 +622,7 @@
|
||||
"ComfyUI Docs": "Документация ComfyUI",
|
||||
"ComfyUI Forum": "Форум ComfyUI",
|
||||
"ComfyUI Issues": "Проблемы ComfyUI",
|
||||
"Contact Support": "Связаться с поддержкой",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||
"Custom Nodes Manager": "Менеджер Пользовательских Узлов",
|
||||
"Delete Selected Items": "Удалить выбранные элементы",
|
||||
@@ -603,6 +649,7 @@
|
||||
"Open Logs Folder": "Открыть папку журналов",
|
||||
"Open Models Folder": "Открыть папку моделей",
|
||||
"Open Outputs Folder": "Открыть папку выходных данных",
|
||||
"Open Sign In Dialog": "Открыть окно входа",
|
||||
"Open extra_model_paths_yaml": "Открыть extra_model_paths.yaml",
|
||||
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
|
||||
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
|
||||
@@ -619,6 +666,7 @@
|
||||
"Save": "Сохранить",
|
||||
"Save As": "Сохранить как",
|
||||
"Show Settings Dialog": "Показать диалог настроек",
|
||||
"Sign Out": "Выйти",
|
||||
"Toggle Bottom Panel": "Переключить нижнюю панель",
|
||||
"Toggle Focus Mode": "Переключить режим фокуса",
|
||||
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
|
||||
@@ -872,6 +920,7 @@
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Десктопный Comfy",
|
||||
"ContextMenu": "Контекстное меню",
|
||||
"Credits": "Кредиты",
|
||||
"CustomColorPalettes": "Пользовательские цветовые палитры",
|
||||
"DevMode": "Режим разработчика",
|
||||
"EditTokenWeight": "Редактировать вес токена",
|
||||
@@ -880,6 +929,7 @@
|
||||
"Graph": "Граф",
|
||||
"Group": "Группа",
|
||||
"Keybinding": "Сочетание клавиш",
|
||||
"Light": "Светлый",
|
||||
"Link": "Ссылка",
|
||||
"LinkRelease": "Освобождение ссылки",
|
||||
"LiteGraph": "Lite Graph",
|
||||
@@ -905,6 +955,7 @@
|
||||
"Sidebar": "Боковая панель",
|
||||
"Tree Explorer": "Дерево проводника",
|
||||
"UV": "UV",
|
||||
"User": "Пользователь",
|
||||
"Validation": "Валидация",
|
||||
"Window": "Окно",
|
||||
"Workflow": "Рабочий процесс"
|
||||
@@ -1059,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 сцены для применения текстуры",
|
||||
@@ -1075,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "Ожидающие задачи удалены",
|
||||
"pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы",
|
||||
"unableToGetModelFilePath": "Не удалось получить путь к файлу модели",
|
||||
"updateRequested": "Запрошено обновление"
|
||||
"updateRequested": "Запрошено обновление",
|
||||
"userNotAuthenticated": "Пользователь не аутентифицирован"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "Введите имя пользователя",
|
||||
@@ -1084,6 +1142,17 @@
|
||||
"next": "Далее",
|
||||
"selectUser": "Выберите пользователя"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "Электронная почта",
|
||||
"name": "Имя",
|
||||
"notSet": "Не задано",
|
||||
"provider": "Способ входа",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "Настройки пользователя"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Недействительный адрес электронной почты",
|
||||
"maxLength": "Должно быть не более {length} символов",
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
"Straight": "Прямой"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Начальный цвет фона",
|
||||
"tooltip": "Управляет цветом фона по умолчанию для 3D-сцены. Этот параметр определяет внешний вид фона при создании нового 3D-виджета, но может быть изменён индивидуально для каждого виджета после создания."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "Тип камеры",
|
||||
"options": {
|
||||
@@ -116,6 +120,22 @@
|
||||
},
|
||||
"tooltip": "Управляет тем, является ли камера перспективной или ортографической по умолчанию при создании нового 3D-виджета. Это значение по умолчанию все еще может быть переключено индивидуально для каждого виджета после его создания."
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "Шаг регулировки освещения",
|
||||
"tooltip": "Определяет размер шага при регулировке интенсивности освещения в 3D-сценах. Меньшее значение шага позволяет более точно настраивать освещение, а большее значение приводит к более заметным изменениям при каждой регулировке."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "Начальная интенсивность освещения",
|
||||
"tooltip": "Устанавливает уровень яркости освещения по умолчанию в 3D-сцене. Это значение определяет, насколько сильно источники света освещают объекты при создании нового 3D-виджета, но может быть изменено для каждого виджета отдельно после создания."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "Максимальная интенсивность света",
|
||||
"tooltip": "Устанавливает максимальное допустимое значение интенсивности света для 3D-сцен. Определяет верхний предел яркости, который можно задать при настройке освещения в любом 3D-виджете."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "Минимальная интенсивность света",
|
||||
"tooltip": "Устанавливает минимально допустимое значение интенсивности света для 3D-сцен. Определяет нижний предел яркости, который можно установить при настройке освещения в любом 3D-виджете."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "Показать сетку",
|
||||
"tooltip": "Переключиться, чтобы показывать сетку по умолчанию"
|
||||
@@ -273,6 +293,9 @@
|
||||
"small": "маленький"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Унифицированная ширина боковой панели"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Размер сетки привязки",
|
||||
"tooltip": "При перетаскивании и изменении размера нод, удерживая shift, они будут выровнены по сетке, это контролирует размер этой сетки."
|
||||
@@ -357,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Масштабирование комбинированных виджетов меню узлов (списков) при увеличении"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Всегда сжимать новые узлы",
|
||||
"tooltip": "Изменять размер узлов до минимально возможного при создании. Если отключено, новый узел будет немного расширен для отображения значений виджетов."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Задержка всплывающей подсказки"
|
||||
},
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "清除工作流"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "联系支持"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "复制当前工作流"
|
||||
},
|
||||
@@ -173,6 +176,12 @@
|
||||
"Comfy_Undo": {
|
||||
"label": "撤销"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "打开登录对话框"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "退出登录"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "关闭当前工作流"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"failed": "登录失败",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"forgotPasswordError": "发送重置密码邮件失败",
|
||||
"loginButton": "登录",
|
||||
"loginWithGithub": "使用Github登录",
|
||||
"loginWithGoogle": "使用Google登录",
|
||||
@@ -24,13 +25,21 @@
|
||||
"orContinueWith": "或者继续使用",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入您的密码",
|
||||
"passwordResetSent": "重置密码邮件已发送",
|
||||
"passwordResetSentDetail": "请查收您的电子邮件,点击链接重置密码。",
|
||||
"privacyLink": "隐私政策",
|
||||
"signInOrSignUp": "登录 / 注册",
|
||||
"signUp": "注册",
|
||||
"success": "登录成功",
|
||||
"termsLink": "使用条款",
|
||||
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
|
||||
"title": "登录您的账户"
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "退出登录",
|
||||
"success": "成功退出登录",
|
||||
"successDetail": "您已成功退出账户。"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "已经有账户了?",
|
||||
"emailLabel": "电子邮件",
|
||||
@@ -92,6 +101,24 @@
|
||||
"Title": "标题",
|
||||
"Unpin": "取消固定"
|
||||
},
|
||||
"credits": {
|
||||
"credits": "积分",
|
||||
"faqs": "常见问题",
|
||||
"invoiceHistory": "发票历史",
|
||||
"lastUpdated": "最近更新",
|
||||
"messageSupport": "联系客服",
|
||||
"purchaseCredits": "购买积分",
|
||||
"topUp": {
|
||||
"buyNow": "立即购买",
|
||||
"insufficientMessage": "您的积分不足,无法运行此工作流。",
|
||||
"insufficientTitle": "积分不足",
|
||||
"maxAmount": "(最高 $1,000 美元)",
|
||||
"quickPurchase": "快速购买",
|
||||
"seeDetails": "查看详情",
|
||||
"topUp": "充值"
|
||||
},
|
||||
"yourCreditBalance": "您的积分余额"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "音频",
|
||||
"BOOLEAN": "布尔",
|
||||
@@ -168,6 +195,7 @@
|
||||
"about": "关于",
|
||||
"add": "添加",
|
||||
"all": "全部",
|
||||
"amount": "数量",
|
||||
"apply": "应用",
|
||||
"back": "返回",
|
||||
"cancel": "取消",
|
||||
@@ -269,11 +297,13 @@
|
||||
"success": "成功",
|
||||
"systemInfo": "系统信息",
|
||||
"terminal": "终端",
|
||||
"unknownError": "未知错误",
|
||||
"update": "更新",
|
||||
"updateAvailable": "有更新可用",
|
||||
"updated": "已更新",
|
||||
"updating": "更新中",
|
||||
"upload": "上传",
|
||||
"user": "用户",
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
@@ -392,19 +422,34 @@
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "跟进联系我",
|
||||
"contactSupportDescription": "请填写下方表格提交您的报告",
|
||||
"contactSupportTitle": "联系支持",
|
||||
"describeTheProblem": "描述问题",
|
||||
"email": "电子邮箱",
|
||||
"feedbackTitle": "通过提供反馈帮助我们改进ComfyUI",
|
||||
"helpFix": "帮助修复这个",
|
||||
"helpTypes": {
|
||||
"billingPayments": "账单 / 支付",
|
||||
"bugReport": "错误报告",
|
||||
"giveFeedback": "提交反馈",
|
||||
"loginAccessIssues": "登录 / 访问问题",
|
||||
"somethingElse": "其他"
|
||||
},
|
||||
"notifyResolve": "解决时通知我",
|
||||
"provideAdditionalDetails": "提供额外的详细信息(可选)",
|
||||
"provideEmail": "提供您的电子邮件(可选)",
|
||||
"rating": "评分",
|
||||
"selectIssue": "选择问题",
|
||||
"stackTrace": "堆栈跟踪",
|
||||
"submitErrorReport": "提交错误报告(可选)",
|
||||
"systemStats": "系统状态",
|
||||
"validation": {
|
||||
"invalidEmail": "请输入有效的电子邮件地址",
|
||||
"maxLength": "消息过长"
|
||||
}
|
||||
"maxLength": "消息过长",
|
||||
"selectIssueType": "请选择一个问题类型"
|
||||
},
|
||||
"whatCanWeInclude": "请说明报告中需要包含的内容",
|
||||
"whatDoYouNeedHelpWith": "您需要什么帮助?"
|
||||
},
|
||||
"load3d": {
|
||||
"applyingTexture": "应用纹理中...",
|
||||
@@ -577,6 +622,7 @@
|
||||
"ComfyUI Docs": "ComfyUI 文档",
|
||||
"ComfyUI Forum": "ComfyUI 论坛",
|
||||
"ComfyUI Issues": "ComfyUI 问题",
|
||||
"Contact Support": "联系支持",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Custom Nodes Manager": "自定义节点管理器",
|
||||
"Delete Selected Items": "删除选定的项目",
|
||||
@@ -603,6 +649,7 @@
|
||||
"Open Logs Folder": "打开日志文件夹",
|
||||
"Open Models Folder": "打开模型文件夹",
|
||||
"Open Outputs Folder": "打开输出文件夹",
|
||||
"Open Sign In Dialog": "打开登录对话框",
|
||||
"Open extra_model_paths_yaml": "打开 extra_model_paths.yaml",
|
||||
"Pin/Unpin Selected Items": "固定/取消固定选定项目",
|
||||
"Pin/Unpin Selected Nodes": "固定/取消固定选定节点",
|
||||
@@ -619,6 +666,7 @@
|
||||
"Save": "保存",
|
||||
"Save As": "另存为",
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
@@ -872,6 +920,7 @@
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfy桌面版",
|
||||
"ContextMenu": "上下文菜单",
|
||||
"Credits": "积分",
|
||||
"CustomColorPalettes": "自定义色彩主题",
|
||||
"DevMode": "开发模式",
|
||||
"EditTokenWeight": "编辑令牌权重",
|
||||
@@ -880,6 +929,7 @@
|
||||
"Graph": "画面",
|
||||
"Group": "组节点",
|
||||
"Keybinding": "快捷键",
|
||||
"Light": "浅色",
|
||||
"Link": "连线",
|
||||
"LinkRelease": "释放链接",
|
||||
"LiteGraph": "画面",
|
||||
@@ -905,6 +955,7 @@
|
||||
"Sidebar": "侧边栏",
|
||||
"Tree Explorer": "树形浏览器",
|
||||
"UV": "UV",
|
||||
"User": "用户",
|
||||
"Validation": "验证",
|
||||
"Window": "窗口",
|
||||
"Workflow": "工作流"
|
||||
@@ -1059,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场景可以应用纹理",
|
||||
@@ -1075,7 +1132,8 @@
|
||||
"pendingTasksDeleted": "待处理任务已删除",
|
||||
"pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组",
|
||||
"unableToGetModelFilePath": "无法获取模型文件路径",
|
||||
"updateRequested": "已请求更新"
|
||||
"updateRequested": "已请求更新",
|
||||
"userNotAuthenticated": "用户未认证"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "输入用户名",
|
||||
@@ -1084,6 +1142,17 @@
|
||||
"next": "下一步",
|
||||
"selectUser": "选择用户"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "电子邮件",
|
||||
"name": "名称",
|
||||
"notSet": "未设置",
|
||||
"provider": "登录方式",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "用户设置"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "无效的电子邮件地址",
|
||||
"maxLength": "不能超过{length}个字符",
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
"Straight": "直角线"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "初始背景颜色",
|
||||
"tooltip": "控制3D场景的默认背景颜色。此设置决定新建3D组件时的背景外观,但每个组件在创建后都可以单独调整。"
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "摄像机类型",
|
||||
"options": {
|
||||
@@ -116,6 +120,22 @@
|
||||
},
|
||||
"tooltip": "控制创建新的3D小部件时,默认的相机是透视还是正交。这个默认设置仍然可以在创建后为每个小部件单独切换。"
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "光照调整步长",
|
||||
"tooltip": "控制在3D场景中调整光照强度时的步长。较小的步长值可以实现更精细的光照调整,较大的值则会使每次调整的变化更加明显。"
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "初始光照强度",
|
||||
"tooltip": "设置3D场景中灯光的默认亮度级别。该数值决定新建3D控件时灯光照亮物体的强度,但每个控件在创建后都可以单独调整。"
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "最大光照强度",
|
||||
"tooltip": "设置3D场景允许的最大光照强度值。此项定义了在调整任何3D控件照明时可设定的最高亮度上限。"
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "光照强度下限",
|
||||
"tooltip": "设置3D场景允许的最小光照强度值。此项定义在调整任何3D控件照明时可设定的最低亮度。"
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "显示网格",
|
||||
"tooltip": "默认显示网格开关"
|
||||
@@ -273,6 +293,9 @@
|
||||
"small": "小"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "统一侧边栏宽度"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "吸附网格大小",
|
||||
"tooltip": "在按住shift拖动和调整节点大小时,节点将对齐到网格,该选项控制网格。"
|
||||
@@ -357,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "放大时缩放节点组合部件菜单(列表)"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "始终收缩新节点",
|
||||
"tooltip": "创建节点时将其缩小到最小可能尺寸。禁用后,新添加的节点会略微加宽以显示控件数值。"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "工具提示延迟"
|
||||
},
|
||||
|
||||