Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
3
.gitignore
vendored
@@ -55,3 +55,6 @@ dist.zip
|
||||
|
||||
# Temporary repository directory
|
||||
templates_repo/
|
||||
|
||||
# Vite’s timestamped config modules
|
||||
vite.config.mts.timestamp-*.mjs
|
||||
|
||||
449
README.md
@@ -67,449 +67,6 @@ The development of successive minor versions overlaps. For example, while versio
|
||||
| 3 | Mar 15-21 | Released | Feature Freeze | Development | 1.1.7 through 1.1.13 (daily)<br>1.2.0 through 1.2.6 (daily) |
|
||||
| 4 | Mar 22-28 | - | Released | Feature Freeze | 1.2.7 through 1.2.13 (daily)<br>1.3.0 through 1.3.6 (daily) |
|
||||
|
||||
## Release Summary
|
||||
|
||||
### Major features
|
||||
|
||||
<details id='feature-native-translation'>
|
||||
<summary>v1.5: Native translation (i18n)</summary>
|
||||
|
||||
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
|
||||
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
|
||||
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
|
||||
|
||||
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
|
||||
</details>
|
||||
|
||||
<details id='feature-mask-editor'>
|
||||
<summary>v1.4: New mask editor</summary>
|
||||
|
||||
https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor.
|
||||
|
||||

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

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

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

|
||||

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

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
</details>
|
||||
|
||||
## Development
|
||||
|
||||
### Tech Stack
|
||||
@@ -557,6 +114,12 @@ After you start the dev server, you should see following logs:
|
||||
Make sure your desktop machine and touch device are on the same network. On your touch device,
|
||||
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
|
||||
|
||||
### Recommended Code Editor Configuration
|
||||
|
||||
This project includes `.vscode/launch.json.default` and `.vscode/settings.json.default` files with recommended launch and workspace settings for editors that use the `.vscode` directory (e.g., VS Code, Cursor, etc.).
|
||||
|
||||
We’ve also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
|
||||
|
||||
### Unit Test
|
||||
|
||||
- `npm i` to install all dependencies
|
||||
|
||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
12
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.17.11",
|
||||
"version": "1.18.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.17.11",
|
||||
"version": "1.18.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.39",
|
||||
"@comfyorg/litegraph": "^0.13.8",
|
||||
"@comfyorg/litegraph": "^0.14.1",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -482,9 +482,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.13.8",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.13.8.tgz",
|
||||
"integrity": "sha512-NpnQpCM0rkAuiWSt7Yp2U9RmNqnK7L9xOye0xHu/avzdOMQTY6vy0g7VKopUrTSGMpeFbgYcnUh/W/XZcCx+sg==",
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.14.1.tgz",
|
||||
"integrity": "sha512-P/OlMsFHgkubvXrLhEJohQO+1e+xx6X/8yTXYFNj5Uz44GaXObaCDv+5guMSHASzuwiKMv4MPJbWSbdjeg5qVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.17.11",
|
||||
"version": "1.18.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -72,7 +72,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.39",
|
||||
"@comfyorg/litegraph": "^0.13.8",
|
||||
"@comfyorg/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(() => {
|
||||
@@ -9,7 +9,7 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
import ManagerProgressDialogContent from '../ManagerProgressDialogContent.vue'
|
||||
import ManagerProgressDialogContent from './ManagerProgressDialogContent.vue'
|
||||
|
||||
type ComponentInstance = InstanceType<typeof ManagerProgressDialogContent> & {
|
||||
lastPanelRef: HTMLElement | null
|
||||
@@ -47,27 +47,10 @@
|
||||
<SettingsPanel :setting-groups="sortedGroups(category)" />
|
||||
</PanelTemplate>
|
||||
|
||||
<AboutPanel />
|
||||
<UserPanel />
|
||||
<CreditsPanel />
|
||||
<Suspense>
|
||||
<KeybindingPanel />
|
||||
<Suspense v-for="panel in panels" :key="panel.node.key">
|
||||
<component :is="panel.component" />
|
||||
<template #fallback>
|
||||
<div>Loading keybinding panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<ExtensionPanel />
|
||||
<template #fallback>
|
||||
<div>Loading extension panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<ServerConfigPanel />
|
||||
<template #fallback>
|
||||
<div>Loading server config panel...</div>
|
||||
<div>Loading {{ panel.node.label }} panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanels>
|
||||
@@ -81,24 +64,21 @@ import Listbox from 'primevue/listbox'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed, defineAsyncComponent, watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { useSettingSearch } from '@/composables/setting/useSettingSearch'
|
||||
import { useSettingUI } from '@/composables/setting/useSettingUI'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { SettingTreeNode } from '@/stores/settingStore'
|
||||
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
|
||||
import AboutPanel from './setting/AboutPanel.vue'
|
||||
import ColorPaletteMessage from './setting/ColorPaletteMessage.vue'
|
||||
import CreditsPanel from './setting/CreditsPanel.vue'
|
||||
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
|
||||
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
|
||||
import PanelTemplate from './setting/PanelTemplate.vue'
|
||||
import SettingsPanel from './setting/SettingsPanel.vue'
|
||||
import UserPanel from './setting/UserPanel.vue'
|
||||
|
||||
const { defaultPanel } = defineProps<{
|
||||
defaultPanel?:
|
||||
@@ -110,21 +90,12 @@ const { defaultPanel } = defineProps<{
|
||||
| 'credits'
|
||||
}>()
|
||||
|
||||
const KeybindingPanel = defineAsyncComponent(
|
||||
() => import('./setting/KeybindingPanel.vue')
|
||||
)
|
||||
const ExtensionPanel = defineAsyncComponent(
|
||||
() => import('./setting/ExtensionPanel.vue')
|
||||
)
|
||||
const ServerConfigPanel = defineAsyncComponent(
|
||||
() => import('./setting/ServerConfigPanel.vue')
|
||||
)
|
||||
|
||||
const {
|
||||
activeCategory,
|
||||
defaultCategory,
|
||||
settingCategories,
|
||||
groupedMenuTreeNodes
|
||||
groupedMenuTreeNodes,
|
||||
panels
|
||||
} = useSettingUI(defaultPanel)
|
||||
|
||||
const {
|
||||
@@ -136,7 +107,7 @@ const {
|
||||
getSearchResults
|
||||
} = useSettingSearch()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authService = useFirebaseAuthService()
|
||||
|
||||
// Sort groups for a category
|
||||
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
|
||||
@@ -169,7 +140,7 @@ watch(activeCategory, (_, oldValue) => {
|
||||
activeCategory.value = oldValue
|
||||
}
|
||||
if (activeCategory.value?.key === 'credits') {
|
||||
void authStore.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,35 +1,67 @@
|
||||
<template>
|
||||
<div class="flex flex-col p-6">
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
:class="{ 'text-red-500': isInsufficientCredits }"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'text-2xl',
|
||||
isInsufficientCredits ? 'pi pi-exclamation-triangle' : ''
|
||||
]"
|
||||
/>
|
||||
<h2 class="text-2xl font-semibold">
|
||||
{{
|
||||
$t(
|
||||
isInsufficientCredits
|
||||
? 'credits.topUp.insufficientTitle'
|
||||
: 'credits.topUp.title'
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<div class="flex flex-col w-96 p-2 gap-10">
|
||||
<div v-if="isInsufficientCredits" class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ $t('credits.topUp.insufficientTitle') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
{{ $t('credits.topUp.insufficientMessage') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<p v-if="isInsufficientCredits" class="text-lg text-muted mt-6">
|
||||
{{ $t('credits.topUp.insufficientMessage') }}
|
||||
</p>
|
||||
|
||||
<!-- Balance Section -->
|
||||
<div class="flex justify-between items-center mt-8">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-muted">{{ $t('credits.yourCreditBalance') }}</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<div class="text-muted text-base">
|
||||
{{ $t('credits.yourCreditBalance') }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-2xl">{{ formattedBalance }}</span>
|
||||
</div>
|
||||
<Button
|
||||
outlined
|
||||
severity="secondary"
|
||||
:label="$t('credits.topUp.seeDetails')"
|
||||
icon="pi pi-arrow-up-right"
|
||||
@click="handleSeeDetails"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount Input Section -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-muted text-sm"
|
||||
>{{ $t('credits.topUp.quickPurchase') }}:</span
|
||||
>
|
||||
<div class="grid grid-cols-[2fr_1fr] gap-2">
|
||||
<template v-for="amount in amountOptions" :key="amount">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-xl">{{ amount }}</span>
|
||||
</div>
|
||||
<Button
|
||||
:severity="
|
||||
preselectedAmountOption === amount ? 'primary' : 'secondary'
|
||||
"
|
||||
:outlined="preselectedAmountOption !== amount"
|
||||
:label="$t('credits.topUp.buyNow')"
|
||||
@click="handleBuyNow(amount)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
@@ -37,122 +69,84 @@
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-2xl">{{ formattedBalance }}</span>
|
||||
<InputNumber
|
||||
v-model="customAmount"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
show-buttons
|
||||
:allow-empty="false"
|
||||
:highlight-on-focus="true"
|
||||
pt:pc-input-text:root="w-24"
|
||||
@blur="
|
||||
(e: InputNumberBlurEvent) => (customAmount = Number(e.value))
|
||||
"
|
||||
@input="
|
||||
(e: InputNumberInputEvent) => (customAmount = Number(e.value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
text
|
||||
severity="secondary"
|
||||
:label="$t('credits.creditsHistory')"
|
||||
icon="pi pi-arrow-up-right"
|
||||
@click="handleSeeDetails"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Amount Input Section -->
|
||||
<div class="flex flex-col gap-2 mt-8">
|
||||
<div>
|
||||
<span class="text-muted">{{ $t('credits.topUp.addCredits') }}</span>
|
||||
<span class="text-muted text-sm ml-1">{{
|
||||
$t('credits.topUp.maxAmount')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||
<Button
|
||||
v-else
|
||||
:label="$t('credits.topUp.buyNow')"
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model="amount"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
show-buttons
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
outlined
|
||||
@click="handleBuyNow(customAmount)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mt-8">
|
||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||
<Button
|
||||
v-else
|
||||
severity="primary"
|
||||
:label="$t('credits.topUp.buyNow')"
|
||||
:disabled="!amount || amount > 1000"
|
||||
:pt="{
|
||||
root: { class: 'px-8' }
|
||||
}"
|
||||
@click="handleBuyNow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import InputNumber, {
|
||||
type InputNumberBlurEvent,
|
||||
type InputNumberInputEvent
|
||||
} from 'primevue/inputnumber'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency, usdToMicros } from '@/utils/formatUtil'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
const {
|
||||
isInsufficientCredits = false,
|
||||
amountOptions = [5, 10, 20, 50],
|
||||
preselectedAmountOption = 10
|
||||
} = defineProps<{
|
||||
isInsufficientCredits?: boolean
|
||||
amountOptions?: number[]
|
||||
preselectedAmountOption?: number
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const amount = ref<number>(9.99)
|
||||
const authService = useFirebaseAuthService()
|
||||
const customAmount = ref<number>(100)
|
||||
const didClickBuyNow = ref(false)
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const handleBlur = (e: any) => {
|
||||
if (e.target.value) {
|
||||
amount.value = parseFloat(e.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = (e: any) => {
|
||||
amount.value = e.value
|
||||
}
|
||||
|
||||
const formattedBalance = computed(() => {
|
||||
if (!authStore.balance) return '0.000'
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
})
|
||||
|
||||
const handleSeeDetails = async () => {
|
||||
const response = await authStore.accessBillingPortal()
|
||||
if (!response?.billing_portal_url) return
|
||||
window.open(response.billing_portal_url, '_blank')
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleBuyNow = async () => {
|
||||
if (!amount.value) return
|
||||
|
||||
const response = await authStore.initiateCreditPurchase({
|
||||
amount_micros: usdToMicros(amount.value),
|
||||
currency: 'usd'
|
||||
})
|
||||
|
||||
if (!response?.checkout_url) return
|
||||
|
||||
const handleBuyNow = async (amount: number) => {
|
||||
await authService.purchaseCredits(amount)
|
||||
didClickBuyNow.value = true
|
||||
|
||||
// Go to Stripe checkout page
|
||||
window.open(response.checkout_url, '_blank')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (didClickBuyNow.value) {
|
||||
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
|
||||
void authStore.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMesages from '@/locales/en/main.json'
|
||||
import { IssueReportPanelProps } from '@/types/issueReportTypes'
|
||||
|
||||
import ReportIssuePanel from '../ReportIssuePanel.vue'
|
||||
import ReportIssuePanel from './ReportIssuePanel.vue'
|
||||
|
||||
const DEFAULT_FIELDS = ['Workflow', 'Logs', 'Settings', 'SystemStats']
|
||||
const CUSTOM_FIELDS = [
|
||||
@@ -142,6 +142,14 @@ vi.mock('@primevue/forms', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
currentUser: {
|
||||
email: 'test@example.com'
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
describe('ReportIssuePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -9,8 +9,8 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionBadge from '../PackVersionBadge.vue'
|
||||
import PackVersionSelectorPopover from '../PackVersionSelectorPopover.vue'
|
||||
import PackVersionBadge from './PackVersionBadge.vue'
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
const mockNodePack = {
|
||||
id: 'test-pack',
|
||||
@@ -10,7 +10,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionSelectorPopover from '../PackVersionSelectorPopover.vue'
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
// Default mock versions for reference
|
||||
const defaultMockVersions = [
|
||||
@@ -9,7 +9,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
import PackEnableToggle from '../PackEnableToggle.vue'
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
// Mock debounce to execute immediately
|
||||
vi.mock('lodash', () => ({
|
||||
@@ -5,7 +5,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
import DescriptionTabPanel from '../DescriptionTabPanel.vue'
|
||||
import DescriptionTabPanel from './DescriptionTabPanel.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -7,8 +7,8 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
import GridSkeleton from '../GridSkeleton.vue'
|
||||
import PackCardSkeleton from '../PackCardSkeleton.vue'
|
||||
import GridSkeleton from './GridSkeleton.vue'
|
||||
import PackCardSkeleton from './PackCardSkeleton.vue'
|
||||
|
||||
describe('GridSkeleton', () => {
|
||||
const mountComponent = ({
|
||||
@@ -51,14 +51,14 @@
|
||||
text
|
||||
size="small"
|
||||
severity="secondary"
|
||||
@click="() => authStore.fetchBalance()"
|
||||
@click="() => authService.fetchBalance()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-8">
|
||||
<Button
|
||||
:label="$t('credits.creditsHistory')"
|
||||
:label="$t('credits.invoiceHistory')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-up-right"
|
||||
@@ -128,6 +128,7 @@ import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
@@ -141,6 +142,7 @@ interface CreditHistoryItemData {
|
||||
const { t } = useI18n()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authService = useFirebaseAuthService()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
const formattedBalance = computed(() => {
|
||||
@@ -159,13 +161,7 @@ const handlePurchaseCreditsClick = () => {
|
||||
}
|
||||
|
||||
const handleCreditsHistoryClick = async () => {
|
||||
const response = await authStore.accessBillingPortal()
|
||||
if (!response) return
|
||||
|
||||
const { billing_portal_url } = response
|
||||
if (billing_portal_url) {
|
||||
window.open(billing_portal_url, '_blank')
|
||||
}
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleMessageSupport = () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SettingItem from '../SettingItem.vue'
|
||||
import SettingItem from './SettingItem.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -5,14 +5,13 @@
|
||||
<Divider class="mb-3" />
|
||||
|
||||
<div v-if="user" class="flex flex-col gap-2">
|
||||
<!-- User Avatar if available -->
|
||||
<div v-if="user.photoURL" class="flex items-center gap-2">
|
||||
<img
|
||||
:src="user.photoURL"
|
||||
:alt="user.displayName || ''"
|
||||
class="w-8 h-8 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<Avatar
|
||||
v-if="user.photoURL"
|
||||
:image="user.photoURL"
|
||||
shape="circle"
|
||||
size="large"
|
||||
aria-label="User Avatar"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h3 class="font-medium">
|
||||
@@ -77,21 +76,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Avatar from 'primevue/avatar'
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const toast = useToastStore()
|
||||
const { t } = useI18n()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const dialogService = useDialogService()
|
||||
const commandStore = useCommandStore()
|
||||
const user = computed(() => authStore.currentUser)
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
@@ -118,20 +114,10 @@ const providerIcon = computed(() => {
|
||||
})
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await authStore.logout()
|
||||
if (authStore.error) {
|
||||
toast.addAlert(authStore.error)
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.signOut.success'),
|
||||
detail: t('auth.signOut.successDetail'),
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
await commandStore.execute('Comfy.User.SignOut')
|
||||
}
|
||||
|
||||
const handleSignIn = async () => {
|
||||
await dialogService.showSignInDialog()
|
||||
await commandStore.execute('Comfy.User.OpenSignInDialog')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -81,14 +81,14 @@ 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'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const firebaseAuthService = useFirebaseAuthService()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToastStore()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [values: SignInData]
|
||||
@@ -102,19 +102,6 @@ const onSubmit = (event: FormSubmitEvent) => {
|
||||
|
||||
const handleForgotPassword = async (email: string) => {
|
||||
if (!email) return
|
||||
await authStore.sendPasswordReset(email)
|
||||
if (authStore.error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('auth.login.forgotPasswordError'),
|
||||
detail: authStore.error
|
||||
})
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.login.passwordResetSent'),
|
||||
detail: t('auth.login.passwordResetSentDetail')
|
||||
})
|
||||
}
|
||||
await firebaseAuthService.sendPasswordReset(email)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -35,8 +35,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CUDA_TORCH_URL,
|
||||
NIGHTLY_CPU_TORCH_URL,
|
||||
TorchDeviceType,
|
||||
TorchMirrorUrl
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
@@ -74,8 +72,8 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
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) {
|
||||
@@ -87,8 +85,8 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
}
|
||||
return {
|
||||
settingId,
|
||||
mirror: CUDA_TORCH_URL,
|
||||
fallbackMirror: CUDA_TORCH_URL
|
||||
mirror: TorchMirrorUrl.Cuda,
|
||||
fallbackMirror: TorchMirrorUrl.Cuda
|
||||
}
|
||||
case 'cpu':
|
||||
default:
|
||||
|
||||
@@ -254,7 +254,7 @@ const cancelNextReset = (e: CustomEvent<CanvasPointerEvent>) => {
|
||||
e.preventDefault()
|
||||
|
||||
const canvas = canvasStore.getCanvas()
|
||||
canvas._highlight_pos = [e.detail.canvasX, e.detail.canvasY]
|
||||
canvas.linkConnector.state.snapLinksPos = [e.detail.canvasX, e.detail.canvasY]
|
||||
useEventListener(canvas.linkConnector.events, 'reset', preventDefault, {
|
||||
once: true
|
||||
})
|
||||
@@ -291,7 +291,6 @@ const reset = () => {
|
||||
if (disconnectOnReset) canvas.linkConnector.disconnectLinks()
|
||||
|
||||
canvas.linkConnector.reset()
|
||||
canvas._highlight_pos = undefined
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import OverlayBadge from 'primevue/overlaybadge'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SidebarIcon from '../SidebarIcon.vue'
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
type SidebarIconProps = {
|
||||
icon: string
|
||||
@@ -10,28 +10,22 @@
|
||||
@click="openUserSettings"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 pr-2 rounded-full bg-[var(--p-content-background)]"
|
||||
class="flex items-center rounded-full bg-[var(--p-content-background)]"
|
||||
>
|
||||
<!-- User Avatar if available -->
|
||||
<div v-if="user?.photoURL" class="flex items-center gap-1">
|
||||
<img
|
||||
:src="user.photoURL"
|
||||
:alt="user.displayName || ''"
|
||||
class="w-8 h-8 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<Avatar
|
||||
:image="photoURL"
|
||||
:icon="photoURL ? undefined : 'pi pi-user'"
|
||||
shape="circle"
|
||||
aria-label="User Avatar"
|
||||
/>
|
||||
|
||||
<!-- User Icon if no avatar -->
|
||||
<div v-else class="w-8 h-8 rounded-full flex items-center justify-center">
|
||||
<i class="pi pi-user text-sm" />
|
||||
</div>
|
||||
|
||||
<i class="pi pi-chevron-down" :style="{ fontSize: '0.5rem' }" />
|
||||
<i class="pi pi-chevron-down px-1" :style="{ fontSize: '0.5rem' }" />
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Avatar from 'primevue/avatar'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -41,7 +35,9 @@ import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const dialogService = useDialogService()
|
||||
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||
const user = computed(() => authStore.currentUser)
|
||||
const photoURL = computed<string | undefined>(
|
||||
() => authStore.currentUser?.photoURL ?? undefined
|
||||
)
|
||||
|
||||
const openUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import {
|
||||
type Component,
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
onMounted,
|
||||
ref
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
@@ -8,6 +14,11 @@ import { isElectron } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
interface SettingPanelItem {
|
||||
node: SettingTreeNode
|
||||
component: Component
|
||||
}
|
||||
|
||||
export function useSettingUI(
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
@@ -48,49 +59,82 @@ export function useSettingUI(
|
||||
() => settingRoot.value.children ?? []
|
||||
)
|
||||
|
||||
// Define panel nodes
|
||||
const aboutPanelNode: SettingTreeNode = {
|
||||
key: 'about',
|
||||
label: 'About',
|
||||
children: []
|
||||
// Define panel items
|
||||
const aboutPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'about',
|
||||
label: 'About',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/AboutPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const creditsPanelNode: SettingTreeNode = {
|
||||
key: 'credits',
|
||||
label: 'Credits',
|
||||
children: []
|
||||
const creditsPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'credits',
|
||||
label: 'Credits',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/CreditsPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const userPanelNode: SettingTreeNode = {
|
||||
key: 'user',
|
||||
label: 'User',
|
||||
children: []
|
||||
const userPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'user',
|
||||
label: 'User',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/UserPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const keybindingPanelNode: SettingTreeNode = {
|
||||
key: 'keybinding',
|
||||
label: 'Keybinding',
|
||||
children: []
|
||||
const keybindingPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'keybinding',
|
||||
label: 'Keybinding',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/KeybindingPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const extensionPanelNode: SettingTreeNode = {
|
||||
key: 'extension',
|
||||
label: 'Extension',
|
||||
children: []
|
||||
const extensionPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'extension',
|
||||
label: 'Extension',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/ExtensionPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const serverConfigPanelNode: SettingTreeNode = {
|
||||
key: 'server-config',
|
||||
label: 'Server-Config',
|
||||
children: []
|
||||
const serverConfigPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'server-config',
|
||||
label: 'Server-Config',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/ServerConfigPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Server config panel is only available in Electron
|
||||
*/
|
||||
const serverConfigPanelNodeList = computed<SettingTreeNode[]>(() => {
|
||||
return isElectron() ? [serverConfigPanelNode] : []
|
||||
})
|
||||
const panels = computed<SettingPanelItem[]>(() =>
|
||||
[
|
||||
aboutPanel,
|
||||
creditsPanel,
|
||||
userPanel,
|
||||
keybindingPanel,
|
||||
extensionPanel
|
||||
].filter((panel) => panel.component)
|
||||
)
|
||||
|
||||
/**
|
||||
* The default category to show when the dialog is opened.
|
||||
@@ -119,8 +163,8 @@ export function useSettingUI(
|
||||
key: 'account',
|
||||
label: 'Account',
|
||||
children: [
|
||||
userPanelNode,
|
||||
...(firebaseAuthStore.isAuthenticated ? [creditsPanelNode] : [])
|
||||
userPanel.node,
|
||||
...(firebaseAuthStore.isAuthenticated ? [creditsPanel.node] : [])
|
||||
].map(translateCategory)
|
||||
},
|
||||
// Normal settings stored in the settingStore
|
||||
@@ -134,10 +178,10 @@ export function useSettingUI(
|
||||
key: 'specialSettings',
|
||||
label: 'Special Settings',
|
||||
children: [
|
||||
keybindingPanelNode,
|
||||
extensionPanelNode,
|
||||
aboutPanelNode,
|
||||
...serverConfigPanelNodeList.value
|
||||
keybindingPanel.node,
|
||||
extensionPanel.node,
|
||||
aboutPanel.node,
|
||||
...(isElectron() ? [serverConfigPanel.node] : [])
|
||||
].map(translateCategory)
|
||||
}
|
||||
])
|
||||
@@ -147,6 +191,7 @@ export function useSettingUI(
|
||||
})
|
||||
|
||||
return {
|
||||
panels,
|
||||
activeCategory,
|
||||
defaultCategory,
|
||||
groupedMenuTreeNodes,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
@@ -31,6 +32,8 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const dialogService = useDialogService()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const firebaseAuthService = useFirebaseAuthService()
|
||||
const toastStore = useToastStore()
|
||||
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
||||
|
||||
const getSelectedNodes = (): LGraphNode[] => {
|
||||
@@ -55,8 +58,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
})
|
||||
}
|
||||
|
||||
const commonProps = { source: 'System' }
|
||||
|
||||
const commands = [
|
||||
{
|
||||
id: 'Comfy.NewBlankWorkflow',
|
||||
@@ -184,7 +185,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Interrupt',
|
||||
function: async () => {
|
||||
await api.interrupt()
|
||||
useToastStore().add({
|
||||
toastStore.add({
|
||||
severity: 'info',
|
||||
summary: t('g.interrupted'),
|
||||
detail: t('toastMessages.interrupted'),
|
||||
@@ -198,7 +199,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Clear Pending Tasks',
|
||||
function: async () => {
|
||||
await useQueueStore().clear(['queue'])
|
||||
useToastStore().add({
|
||||
toastStore.add({
|
||||
severity: 'info',
|
||||
summary: t('g.confirmed'),
|
||||
detail: t('toastMessages.pendingTasksDeleted'),
|
||||
@@ -246,7 +247,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Fit view to selected nodes',
|
||||
function: () => {
|
||||
if (app.canvas.empty) {
|
||||
useToastStore().add({
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.emptyCanvas'),
|
||||
life: 3000
|
||||
@@ -328,7 +329,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
const { canvas } = app
|
||||
if (!canvas.selectedItems?.size) {
|
||||
useToastStore().add({
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.nothingToGroup'),
|
||||
detail: t('toastMessages.pleaseSelectNodesToGroup'),
|
||||
@@ -641,8 +642,17 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: async () => {
|
||||
await dialogService.showSignInDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.User.SignOut',
|
||||
icon: 'pi pi-sign-out',
|
||||
label: 'Sign Out',
|
||||
versionAdded: '1.18.1',
|
||||
function: async () => {
|
||||
await firebaseAuthService.logout()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return commands.map((command) => ({ ...command, ...commonProps }))
|
||||
return commands.map((command) => ({ ...command, source: 'System' }))
|
||||
}
|
||||
|
||||
@@ -88,6 +88,14 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
// Default to small if the window is less than 1536px(2xl) wide.
|
||||
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Sidebar.UnifiedWidth',
|
||||
category: ['Appearance', 'Sidebar', 'UnifiedWidth'],
|
||||
name: 'Unified sidebar width',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.18.1'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TextareaWidget.FontSize',
|
||||
category: ['Appearance', 'Node Widget', 'TextareaWidget', 'FontSize'],
|
||||
@@ -794,5 +802,14 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.16.1'
|
||||
},
|
||||
{
|
||||
id: 'LiteGraph.Node.DefaultPadding',
|
||||
name: 'Always shrink new nodes',
|
||||
tooltip:
|
||||
'Resize nodes to the smallest possible size when created. When disabled, a newly added node will be widened slightly to show widget values.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.18.0'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,42 +2,35 @@ import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let touchZooming
|
||||
let touchZooming = false
|
||||
let touchCount = 0
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.SimpleTouchSupport',
|
||||
setup() {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let touchDist
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let touchTime
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let lastTouch
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let lastScale
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function getMultiTouchPos(e) {
|
||||
let touchDist: number | null = null
|
||||
let touchTime: Date | null = null
|
||||
let lastTouch: { clientX: number; clientY: number } | null = null
|
||||
let lastScale: number | null = null
|
||||
function getMultiTouchPos(e: TouchEvent) {
|
||||
return Math.hypot(
|
||||
e.touches[0].clientX - e.touches[1].clientX,
|
||||
e.touches[0].clientY - e.touches[1].clientY
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function getMultiTouchCenter(e) {
|
||||
function getMultiTouchCenter(e: TouchEvent) {
|
||||
return {
|
||||
clientX: (e.touches[0].clientX + e.touches[1].clientX) / 2,
|
||||
clientY: (e.touches[0].clientY + e.touches[1].clientY) / 2
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
app.canvasEl.parentElement.addEventListener(
|
||||
app.canvasEl.parentElement?.addEventListener(
|
||||
'touchstart',
|
||||
(e: TouchEvent) => {
|
||||
touchCount++
|
||||
touchCount += e.changedTouches.length
|
||||
|
||||
lastTouch = null
|
||||
lastScale = null
|
||||
if (e.touches?.length === 1) {
|
||||
@@ -59,35 +52,48 @@ app.registerExtension({
|
||||
true
|
||||
)
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
app.canvasEl.parentElement.addEventListener('touchend', (e: TouchEvent) => {
|
||||
touchCount--
|
||||
app.canvasEl.parentElement?.addEventListener(
|
||||
'touchend',
|
||||
(e: TouchEvent) => {
|
||||
touchCount -= e.changedTouches.length
|
||||
|
||||
if (e.touches?.length !== 1) touchZooming = false
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (touchTime && !e.touches?.length) {
|
||||
if (new Date().getTime() - touchTime > 600) {
|
||||
if (e.target === app.canvasEl) {
|
||||
app.canvasEl.dispatchEvent(
|
||||
new PointerEvent('pointerdown', {
|
||||
button: 2,
|
||||
if (e.touches?.length !== 1) touchZooming = false
|
||||
if (touchTime && !e.touches?.length) {
|
||||
if (new Date().getTime() - touchTime.getTime() > 600) {
|
||||
if (e.target === app.canvasEl) {
|
||||
const touch = {
|
||||
button: 2, // Right click
|
||||
clientX: e.changedTouches[0].clientX,
|
||||
clientY: e.changedTouches[0].clientY
|
||||
clientY: e.changedTouches[0].clientY,
|
||||
pointerId: 1, // changedTouches' id is 0, set it to any number
|
||||
isPrimary: true // changedTouches' isPrimary is false, so set it to true
|
||||
}
|
||||
// context menu info set in 'pointerdown' event
|
||||
app.canvasEl.dispatchEvent(new PointerEvent('pointerdown', touch))
|
||||
// then, context menu open after 'pointerup' event
|
||||
setTimeout(() => {
|
||||
app.canvasEl.dispatchEvent(new PointerEvent('pointerup', touch))
|
||||
})
|
||||
)
|
||||
e.preventDefault()
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
touchTime = null
|
||||
}
|
||||
touchTime = null
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
app.canvasEl.parentElement.addEventListener(
|
||||
app.canvasEl.parentElement?.addEventListener(
|
||||
'touchmove',
|
||||
(e) => {
|
||||
touchTime = null
|
||||
// @ts-expect-error fixme ts strict error
|
||||
// make a threshold for touchmove to prevent clear touchTime for long press
|
||||
if (touchTime && lastTouch && e.touches?.length === 1) {
|
||||
const onlyTouch = e.touches[0]
|
||||
const deltaX = onlyTouch.clientX - lastTouch.clientX
|
||||
const deltaY = onlyTouch.clientY - lastTouch.clientY
|
||||
if (deltaX * deltaX + deltaY * deltaY > 30) {
|
||||
touchTime = null
|
||||
}
|
||||
}
|
||||
if (e.touches?.length === 2 && lastTouch && !e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault() // Prevent browser from zooming when two textareas are touched
|
||||
app.canvas.pointer.isDown = false
|
||||
@@ -100,7 +106,7 @@ app.registerExtension({
|
||||
|
||||
const center = getMultiTouchCenter(e)
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (lastScale === null || touchDist === null) return
|
||||
let scale = (lastScale * newTouchDist) / touchDist
|
||||
|
||||
const newX = (center.clientX - lastTouch.clientX) / scale
|
||||
@@ -124,8 +130,7 @@ app.registerExtension({
|
||||
|
||||
const newScale = app.canvas.ds.scale
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const convertScaleToOffset = (scale) => [
|
||||
const convertScaleToOffset = (scale: number) => [
|
||||
center.clientX / scale - app.canvas.ds.offset[0],
|
||||
center.clientY / scale - app.canvas.ds.offset[1]
|
||||
]
|
||||
@@ -147,22 +152,18 @@ app.registerExtension({
|
||||
})
|
||||
|
||||
const processMouseDown = LGraphCanvas.prototype.processMouseDown
|
||||
LGraphCanvas.prototype.processMouseDown = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
LGraphCanvas.prototype.processMouseDown = function (e: PointerEvent) {
|
||||
if (touchZooming || touchCount) {
|
||||
return
|
||||
}
|
||||
app.canvas.pointer.isDown = false // Prevent context menu from opening on second tap
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return processMouseDown.apply(this, arguments)
|
||||
return processMouseDown.apply(this, [e])
|
||||
}
|
||||
|
||||
const processMouseMove = LGraphCanvas.prototype.processMouseMove
|
||||
LGraphCanvas.prototype.processMouseMove = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
LGraphCanvas.prototype.processMouseMove = function (e: PointerEvent) {
|
||||
if (touchZooming || touchCount > 1) {
|
||||
return
|
||||
}
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return processMouseMove.apply(this, arguments)
|
||||
return processMouseMove.apply(this, [e])
|
||||
}
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Open Sign In Dialog"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Sign Out"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Close Current Workflow"
|
||||
},
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
"updateAvailable": "Update Available",
|
||||
"login": "Login",
|
||||
"learnMore": "Learn more",
|
||||
"amount": "Amount"
|
||||
"amount": "Amount",
|
||||
"unknownError": "Unknown error"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -678,6 +679,7 @@
|
||||
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
|
||||
"Undo": "Undo",
|
||||
"Open Sign In Dialog": "Open Sign In Dialog",
|
||||
"Sign Out": "Sign Out",
|
||||
"Close Current Workflow": "Close Current Workflow",
|
||||
"Next Opened Workflow": "Next Opened Workflow",
|
||||
"Previous Opened Workflow": "Previous Opened Workflow",
|
||||
@@ -1076,7 +1078,14 @@
|
||||
"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": {
|
||||
@@ -1103,8 +1112,7 @@
|
||||
"andText": "and",
|
||||
"privacyLink": "Privacy Policy",
|
||||
"success": "Login successful",
|
||||
"failed": "Login failed",
|
||||
"genericErrorMessage": "Sorry, we've encountered an error. Please contact {supportEmail}."
|
||||
"failed": "Login failed"
|
||||
},
|
||||
"signup": {
|
||||
"title": "Create an account",
|
||||
@@ -1143,17 +1151,17 @@
|
||||
"credits": "Credits",
|
||||
"yourCreditBalance": "Your credit balance",
|
||||
"purchaseCredits": "Purchase Credits",
|
||||
"creditsHistory": "Credits History",
|
||||
"invoiceHistory": "Invoice History",
|
||||
"faqs": "FAQs",
|
||||
"messageSupport": "Message Support",
|
||||
"lastUpdated": "Last updated",
|
||||
"topUp": {
|
||||
"title": "Add to Credit Balance",
|
||||
"insufficientTitle": "Insufficient Credits",
|
||||
"insufficientMessage": "You don't have enough credits to run this workflow.",
|
||||
"addCredits": "Add credits to your balance",
|
||||
"quickPurchase": "Quick Purchase",
|
||||
"maxAmount": "(Max. $1,000 USD)",
|
||||
"buyNow": "Buy now"
|
||||
"buyNow": "Buy now",
|
||||
"seeDetails": "See details"
|
||||
}
|
||||
},
|
||||
"userSettings": {
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "small"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Unified sidebar width"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Snap to grid size",
|
||||
"tooltip": "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Scale node combo widget menus (lists) when zoomed in"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Always shrink new nodes",
|
||||
"tooltip": "Resize nodes to the smallest possible size when created. When disabled, a newly added node will be widened slightly to show widget values."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Tooltip Delay"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Abrir diálogo de inicio de sesión"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Cerrar sesión"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Cerrar Flujo de Trabajo Actual"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"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",
|
||||
"genericErrorMessage": "Lo sentimos, hemos encontrado un error. Por favor, contacta con {supportEmail}.",
|
||||
"loginButton": "Iniciar sesión",
|
||||
"loginWithGithub": "Iniciar sesión con Github",
|
||||
"loginWithGoogle": "Iniciar sesión con Google",
|
||||
@@ -104,18 +103,18 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Créditos",
|
||||
"creditsHistory": "Historial de créditos",
|
||||
"faqs": "Preguntas frecuentes",
|
||||
"invoiceHistory": "Historial de facturas",
|
||||
"lastUpdated": "Última actualización",
|
||||
"messageSupport": "Contactar soporte",
|
||||
"purchaseCredits": "Comprar créditos",
|
||||
"topUp": {
|
||||
"addCredits": "Agregar créditos a tu saldo",
|
||||
"buyNow": "Comprar ahora",
|
||||
"insufficientMessage": "No tienes suficientes créditos para ejecutar este flujo de trabajo.",
|
||||
"insufficientTitle": "Créditos insuficientes",
|
||||
"maxAmount": "(Máx. $1,000 USD)",
|
||||
"title": "Agregar al saldo de créditos"
|
||||
"quickPurchase": "Compra rápida",
|
||||
"seeDetails": "Ver detalles"
|
||||
},
|
||||
"yourCreditBalance": "Tu saldo de créditos"
|
||||
},
|
||||
@@ -297,6 +296,7 @@
|
||||
"success": "Éxito",
|
||||
"systemInfo": "Información del sistema",
|
||||
"terminal": "Terminal",
|
||||
"unknownError": "Error desconocido",
|
||||
"update": "Actualizar",
|
||||
"updateAvailable": "Actualización Disponible",
|
||||
"updated": "Actualizado",
|
||||
@@ -664,6 +664,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",
|
||||
@@ -1107,12 +1108,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",
|
||||
@@ -1123,7 +1130,8 @@
|
||||
"pendingTasksDeleted": "Tareas pendientes eliminadas",
|
||||
"pleaseSelectNodesToGroup": "Por favor, seleccione los nodos (u otros grupos) para crear un grupo para",
|
||||
"unableToGetModelFilePath": "No se puede obtener la ruta del archivo del modelo",
|
||||
"updateRequested": "Actualización solicitada"
|
||||
"updateRequested": "Actualización solicitada",
|
||||
"userNotAuthenticated": "Usuario no autenticado"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "Introduce un nombre de usuario",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "pequeña"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Ancho unificado de la barra lateral"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Tamaño de la cuadrícula para ajustar",
|
||||
"tooltip": "Al arrastrar y redimensionar nodos mientras se mantiene presionada la tecla shift, se alinearán a la cuadrícula, esto controla el tamaño de esa cuadrícula."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Escala los menús de widgets combinados de nodos (listas) al acercar"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Reducir siempre los nuevos nodos",
|
||||
"tooltip": "Redimensiona los nodos al tamaño más pequeño posible al crearlos. Si está desactivado, un nodo recién añadido se ampliará ligeramente para mostrar los valores de los widgets."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Retraso de la información sobre herramientas"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Ouvrir la boîte de dialogue de connexion"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Se déconnecter"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Fermer le flux de travail actuel"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"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",
|
||||
"genericErrorMessage": "Désolé, une erreur s'est produite. Veuillez contacter {supportEmail}.",
|
||||
"loginButton": "Se connecter",
|
||||
"loginWithGithub": "Se connecter avec Github",
|
||||
"loginWithGoogle": "Se connecter avec Google",
|
||||
@@ -104,18 +103,18 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Crédits",
|
||||
"creditsHistory": "Historique des crédits",
|
||||
"faqs": "FAQ",
|
||||
"invoiceHistory": "Historique des factures",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"messageSupport": "Contacter le support",
|
||||
"purchaseCredits": "Acheter des crédits",
|
||||
"topUp": {
|
||||
"addCredits": "Ajouter des crédits à votre solde",
|
||||
"buyNow": "Acheter maintenant",
|
||||
"insufficientMessage": "Vous n'avez pas assez de crédits pour exécuter ce workflow.",
|
||||
"insufficientTitle": "Crédits insuffisants",
|
||||
"maxAmount": "(Max. 1 000 $ US)",
|
||||
"title": "Ajouter au solde de crédits"
|
||||
"quickPurchase": "Achat rapide",
|
||||
"seeDetails": "Voir les détails"
|
||||
},
|
||||
"yourCreditBalance": "Votre solde de crédits"
|
||||
},
|
||||
@@ -297,6 +296,7 @@
|
||||
"success": "Succès",
|
||||
"systemInfo": "Informations système",
|
||||
"terminal": "Terminal",
|
||||
"unknownError": "Erreur inconnue",
|
||||
"update": "Mettre à jour",
|
||||
"updateAvailable": "Mise à jour disponible",
|
||||
"updated": "Mis à jour",
|
||||
@@ -664,6 +664,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",
|
||||
@@ -1107,12 +1108,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",
|
||||
@@ -1123,7 +1130,8 @@
|
||||
"pendingTasksDeleted": "Tâches en attente supprimées",
|
||||
"pleaseSelectNodesToGroup": "Veuillez sélectionner les nœuds (ou autres groupes) pour créer un groupe pour",
|
||||
"unableToGetModelFilePath": "Impossible d'obtenir le chemin du fichier modèle",
|
||||
"updateRequested": "Mise à jour demandée"
|
||||
"updateRequested": "Mise à jour demandée",
|
||||
"userNotAuthenticated": "Utilisateur non authentifié"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "Entrez un nom d'utilisateur",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "petit"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Largeur unifiée de la barre latérale"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Taille de la grille d'alignement",
|
||||
"tooltip": "Lors du déplacement et du redimensionnement des nœuds tout en maintenant shift, ils seront alignés sur la grille, cela contrôle la taille de cette grille."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Mise à l'échelle des menus de widgets combinés de nœuds (listes) lors du zoom"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Toujours réduire les nouveaux nœuds",
|
||||
"tooltip": "Redimensionner les nœuds à la taille minimale possible lors de leur création. Lorsque cette option est désactivée, un nœud nouvellement ajouté sera légèrement élargi pour afficher les valeurs des widgets."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Délai d'infobulle"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "サインインダイアログを開く"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "サインアウト"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "現在のワークフローを閉じる"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"failed": "ログイン失敗",
|
||||
"forgotPassword": "パスワードを忘れましたか?",
|
||||
"forgotPasswordError": "パスワードリセット用メールの送信に失敗しました",
|
||||
"genericErrorMessage": "申し訳ありませんが、エラーが発生しました。{supportEmail} までご連絡ください。",
|
||||
"loginButton": "ログイン",
|
||||
"loginWithGithub": "Githubでログイン",
|
||||
"loginWithGoogle": "Googleでログイン",
|
||||
@@ -104,18 +103,18 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "クレジット",
|
||||
"creditsHistory": "クレジット履歴",
|
||||
"faqs": "よくある質問",
|
||||
"invoiceHistory": "請求履歴",
|
||||
"lastUpdated": "最終更新",
|
||||
"messageSupport": "サポートにメッセージ",
|
||||
"purchaseCredits": "クレジットを購入",
|
||||
"topUp": {
|
||||
"addCredits": "残高にクレジットを追加",
|
||||
"buyNow": "今すぐ購入",
|
||||
"insufficientMessage": "このワークフローを実行するのに十分なクレジットがありません。",
|
||||
"insufficientTitle": "クレジット不足",
|
||||
"maxAmount": "(最大 $1,000 USD)",
|
||||
"title": "クレジット残高を追加"
|
||||
"quickPurchase": "クイック購入",
|
||||
"seeDetails": "詳細を見る"
|
||||
},
|
||||
"yourCreditBalance": "あなたのクレジット残高"
|
||||
},
|
||||
@@ -297,6 +296,7 @@
|
||||
"success": "成功",
|
||||
"systemInfo": "システム情報",
|
||||
"terminal": "ターミナル",
|
||||
"unknownError": "不明なエラー",
|
||||
"update": "更新",
|
||||
"updateAvailable": "更新が利用可能",
|
||||
"updated": "更新済み",
|
||||
@@ -664,6 +664,7 @@
|
||||
"Save": "保存",
|
||||
"Save As": "名前を付けて保存",
|
||||
"Show Settings Dialog": "設定ダイアログを表示",
|
||||
"Sign Out": "サインアウト",
|
||||
"Toggle Bottom Panel": "下部パネルの切り替え",
|
||||
"Toggle Focus Mode": "フォーカスモードの切り替え",
|
||||
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
|
||||
@@ -1107,12 +1108,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シーンがありません",
|
||||
@@ -1123,7 +1130,8 @@
|
||||
"pendingTasksDeleted": "保留中のタスクが削除されました",
|
||||
"pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください",
|
||||
"unableToGetModelFilePath": "モデルファイルのパスを取得できません",
|
||||
"updateRequested": "更新が要求されました"
|
||||
"updateRequested": "更新が要求されました",
|
||||
"userNotAuthenticated": "ユーザーが認証されていません"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "ユーザー名を入力してください",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "小"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "サイドバーの幅を統一"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "グリッドサイズにスナップ",
|
||||
"tooltip": "シフトを押しながらノードをドラッグおよびサイズ変更すると、グリッドに整列されます。これにより、そのグリッドのサイズが制御されます。"
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "ズームイン時にノードコンボウィジェットメニュー(リスト)をスケーリングする"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "新しいノードを常に縮小",
|
||||
"tooltip": "ノード作成時に可能な限り小さいサイズにリサイズします。無効にすると、新しく追加されたノードはウィジェットの値が表示されるように少し幅広くなります。"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "ツールチップ遅延"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "로그인 대화상자 열기"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "로그아웃"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "현재 워크플로우 닫기"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"failed": "로그인 실패",
|
||||
"forgotPassword": "비밀번호를 잊으셨나요?",
|
||||
"forgotPasswordError": "비밀번호 재설정 이메일 전송에 실패했습니다",
|
||||
"genericErrorMessage": "죄송합니다. 오류가 발생했습니다. {supportEmail}로 문의해 주세요.",
|
||||
"loginButton": "로그인",
|
||||
"loginWithGithub": "Github로 로그인",
|
||||
"loginWithGoogle": "구글로 로그인",
|
||||
@@ -104,18 +103,18 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "크레딧",
|
||||
"creditsHistory": "크레딧 내역",
|
||||
"faqs": "자주 묻는 질문",
|
||||
"invoiceHistory": "청구서 내역",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"messageSupport": "지원 문의",
|
||||
"purchaseCredits": "크레딧 구매",
|
||||
"topUp": {
|
||||
"addCredits": "잔액에 크레딧 추가",
|
||||
"buyNow": "지금 구매",
|
||||
"insufficientMessage": "이 워크플로우를 실행하기에 크레딧이 부족합니다.",
|
||||
"insufficientTitle": "크레딧 부족",
|
||||
"maxAmount": "(최대 $1,000 USD)",
|
||||
"title": "크레딧 잔액 충전"
|
||||
"quickPurchase": "빠른 구매",
|
||||
"seeDetails": "자세히 보기"
|
||||
},
|
||||
"yourCreditBalance": "보유 크레딧 잔액"
|
||||
},
|
||||
@@ -297,6 +296,7 @@
|
||||
"success": "성공",
|
||||
"systemInfo": "시스템 정보",
|
||||
"terminal": "터미널",
|
||||
"unknownError": "알 수 없는 오류",
|
||||
"update": "업데이트",
|
||||
"updateAvailable": "업데이트 가능",
|
||||
"updated": "업데이트 됨",
|
||||
@@ -664,6 +664,7 @@
|
||||
"Save": "저장",
|
||||
"Save As": "다른 이름으로 저장",
|
||||
"Show Settings Dialog": "설정 대화상자 표시",
|
||||
"Sign Out": "로그아웃",
|
||||
"Toggle Bottom Panel": "하단 패널 전환",
|
||||
"Toggle Focus Mode": "포커스 모드 전환",
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
@@ -1107,12 +1108,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 장면이 없습니다",
|
||||
@@ -1123,7 +1130,8 @@
|
||||
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",
|
||||
"pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요",
|
||||
"unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다",
|
||||
"updateRequested": "업데이트 요청됨"
|
||||
"updateRequested": "업데이트 요청됨",
|
||||
"userNotAuthenticated": "사용자가 인증되지 않았습니다"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "사용자 이름 입력",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "작음"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "통합 사이드바 너비"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "그리드 크기에 스냅",
|
||||
"tooltip": "Shift 키를 누른 상태에서 노드를 드래그하거나 크기를 조절하면 그리드에 맞춰 정렬됩니다. 이 설정은 그리드의 크기를 제어합니다."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "확대시 노드 콤보 위젯 메뉴 (목록) 스케일링"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "새 노드를 항상 축소",
|
||||
"tooltip": "노드를 생성할 때 가능한 가장 작은 크기로 크기를 조정합니다. 비활성화하면 새로 추가된 노드가 위젯 값을 표시하기 위해 약간 넓어집니다."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "툴팁 지연"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "Открыть окно входа"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "Выйти"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "Закрыть текущий рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"failed": "Вход не удался",
|
||||
"forgotPassword": "Забыли пароль?",
|
||||
"forgotPasswordError": "Не удалось отправить письмо для сброса пароля",
|
||||
"genericErrorMessage": "Извините, произошла ошибка. Пожалуйста, свяжитесь с {supportEmail}.",
|
||||
"loginButton": "Войти",
|
||||
"loginWithGithub": "Войти через Github",
|
||||
"loginWithGoogle": "Войти через Google",
|
||||
@@ -104,18 +103,18 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "Кредиты",
|
||||
"creditsHistory": "История кредитов",
|
||||
"faqs": "Часто задаваемые вопросы",
|
||||
"invoiceHistory": "История счетов",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"messageSupport": "Связаться с поддержкой",
|
||||
"purchaseCredits": "Купить кредиты",
|
||||
"topUp": {
|
||||
"addCredits": "Добавить кредиты на баланс",
|
||||
"buyNow": "Купить сейчас",
|
||||
"insufficientMessage": "У вас недостаточно кредитов для запуска этого рабочего процесса.",
|
||||
"insufficientTitle": "Недостаточно кредитов",
|
||||
"maxAmount": "(Макс. $1,000 USD)",
|
||||
"title": "Пополнить баланс кредитов"
|
||||
"quickPurchase": "Быстрая покупка",
|
||||
"seeDetails": "Смотреть детали"
|
||||
},
|
||||
"yourCreditBalance": "Ваш баланс кредитов"
|
||||
},
|
||||
@@ -297,6 +296,7 @@
|
||||
"success": "Успех",
|
||||
"systemInfo": "Информация о системе",
|
||||
"terminal": "Терминал",
|
||||
"unknownError": "Неизвестная ошибка",
|
||||
"update": "Обновить",
|
||||
"updateAvailable": "Доступно обновление",
|
||||
"updated": "Обновлено",
|
||||
@@ -664,6 +664,7 @@
|
||||
"Save": "Сохранить",
|
||||
"Save As": "Сохранить как",
|
||||
"Show Settings Dialog": "Показать диалог настроек",
|
||||
"Sign Out": "Выйти",
|
||||
"Toggle Bottom Panel": "Переключить нижнюю панель",
|
||||
"Toggle Focus Mode": "Переключить режим фокуса",
|
||||
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
|
||||
@@ -1107,12 +1108,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 сцены для применения текстуры",
|
||||
@@ -1123,7 +1130,8 @@
|
||||
"pendingTasksDeleted": "Ожидающие задачи удалены",
|
||||
"pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы",
|
||||
"unableToGetModelFilePath": "Не удалось получить путь к файлу модели",
|
||||
"updateRequested": "Запрошено обновление"
|
||||
"updateRequested": "Запрошено обновление",
|
||||
"userNotAuthenticated": "Пользователь не аутентифицирован"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "Введите имя пользователя",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "маленький"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "Унифицированная ширина боковой панели"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Размер сетки привязки",
|
||||
"tooltip": "При перетаскивании и изменении размера нод, удерживая shift, они будут выровнены по сетке, это контролирует размер этой сетки."
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Масштабирование комбинированных виджетов меню узлов (списков) при увеличении"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "Всегда сжимать новые узлы",
|
||||
"tooltip": "Изменять размер узлов до минимально возможного при создании. Если отключено, новый узел будет немного расширен для отображения значений виджетов."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Задержка всплывающей подсказки"
|
||||
},
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "打开登录对话框"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "退出登录"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "关闭当前工作流"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"failed": "登录失败",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"forgotPasswordError": "发送重置密码邮件失败",
|
||||
"genericErrorMessage": "抱歉,我们遇到了一些错误。请联系 {supportEmail}。",
|
||||
"loginButton": "登录",
|
||||
"loginWithGithub": "使用Github登录",
|
||||
"loginWithGoogle": "使用Google登录",
|
||||
@@ -104,18 +103,18 @@
|
||||
},
|
||||
"credits": {
|
||||
"credits": "积分",
|
||||
"creditsHistory": "积分历史",
|
||||
"faqs": "常见问题",
|
||||
"invoiceHistory": "发票历史",
|
||||
"lastUpdated": "最近更新",
|
||||
"messageSupport": "联系客服",
|
||||
"purchaseCredits": "购买积分",
|
||||
"topUp": {
|
||||
"addCredits": "为您的余额充值",
|
||||
"buyNow": "立即购买",
|
||||
"insufficientMessage": "您的积分不足,无法运行此工作流。",
|
||||
"insufficientTitle": "积分不足",
|
||||
"maxAmount": "(最高 $1,000 美元)",
|
||||
"title": "充值余额"
|
||||
"quickPurchase": "快速购买",
|
||||
"seeDetails": "查看详情"
|
||||
},
|
||||
"yourCreditBalance": "您的积分余额"
|
||||
},
|
||||
@@ -297,6 +296,7 @@
|
||||
"success": "成功",
|
||||
"systemInfo": "系统信息",
|
||||
"terminal": "终端",
|
||||
"unknownError": "未知错误",
|
||||
"update": "更新",
|
||||
"updateAvailable": "有更新可用",
|
||||
"updated": "已更新",
|
||||
@@ -664,6 +664,7 @@
|
||||
"Save": "保存",
|
||||
"Save As": "另存为",
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
@@ -1107,12 +1108,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场景可以应用纹理",
|
||||
@@ -1123,7 +1130,8 @@
|
||||
"pendingTasksDeleted": "待处理任务已删除",
|
||||
"pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组",
|
||||
"unableToGetModelFilePath": "无法获取模型文件路径",
|
||||
"updateRequested": "已请求更新"
|
||||
"updateRequested": "已请求更新",
|
||||
"userNotAuthenticated": "用户未认证"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "输入用户名",
|
||||
|
||||
@@ -293,6 +293,9 @@
|
||||
"small": "小"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "统一侧边栏宽度"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "吸附网格大小",
|
||||
"tooltip": "在按住shift拖动和调整节点大小时,节点将对齐到网格,该选项控制网格。"
|
||||
@@ -377,6 +380,10 @@
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "放大时缩放节点组合部件菜单(列表)"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "始终收缩新节点",
|
||||
"tooltip": "创建节点时将其缩小到最小可能尺寸。禁用后,新添加的节点会略微加宽以显示控件数值。"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "工具提示延迟"
|
||||
},
|
||||
|
||||
@@ -384,6 +384,7 @@ const zSettings = z.object({
|
||||
'Comfy.PromptFilename': z.boolean(),
|
||||
'Comfy.Sidebar.Location': z.enum(['left', 'right']),
|
||||
'Comfy.Sidebar.Size': z.enum(['small', 'normal']),
|
||||
'Comfy.Sidebar.UnifiedWidth': z.boolean(),
|
||||
'Comfy.SwitchUser': z.any(),
|
||||
'Comfy.SnapToGrid.GridSize': z.number(),
|
||||
'Comfy.TextareaWidget.FontSize': z.number(),
|
||||
@@ -454,7 +455,8 @@ const zSettings = z.object({
|
||||
/** Settings used for testing */
|
||||
'test.setting': z.any(),
|
||||
'main.sub.setting.name': z.any(),
|
||||
'single.setting': z.any()
|
||||
'single.setting': z.any(),
|
||||
'LiteGraph.Node.DefaultPadding': z.boolean()
|
||||
})
|
||||
|
||||
export type EmbeddingsResponse = z.infer<typeof zEmbeddingsResponse>
|
||||
|
||||
@@ -25,6 +25,7 @@ export const zBaseInputOptions = z
|
||||
tooltip: z.string().optional(),
|
||||
hidden: z.boolean().optional(),
|
||||
advanced: z.boolean().optional(),
|
||||
widgetType: z.string().optional(),
|
||||
/** Backend-only properties. */
|
||||
rawLink: z.boolean().optional(),
|
||||
lazy: z.boolean().optional()
|
||||
|
||||
90
src/services/firebaseAuthService.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Service for Firebase Auth actions.
|
||||
* All actions are wrapped with error handling.
|
||||
* @returns {Object} - Object containing all Firebase Auth actions
|
||||
*/
|
||||
export const useFirebaseAuthService = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const toastStore = useToastStore()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const reportError = (error: unknown) => {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
|
||||
const logout = wrapWithErrorHandlingAsync(async () => {
|
||||
await authStore.logout()
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.signOut.success'),
|
||||
detail: t('auth.signOut.successDetail'),
|
||||
life: 5000
|
||||
})
|
||||
}, reportError)
|
||||
|
||||
const sendPasswordReset = wrapWithErrorHandlingAsync(
|
||||
async (email: string) => {
|
||||
await authStore.sendPasswordReset(email)
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.login.passwordResetSent'),
|
||||
detail: t('auth.login.passwordResetSentDetail'),
|
||||
life: 5000
|
||||
})
|
||||
},
|
||||
reportError
|
||||
)
|
||||
|
||||
const purchaseCredits = wrapWithErrorHandlingAsync(async (amount: number) => {
|
||||
const response = await authStore.initiateCreditPurchase({
|
||||
amount_micros: usdToMicros(amount),
|
||||
currency: 'usd'
|
||||
})
|
||||
|
||||
if (!response.checkout_url) {
|
||||
throw new Error(
|
||||
t('toastMessages.failedToPurchaseCredits', {
|
||||
error: 'No checkout URL returned'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Go to Stripe checkout page
|
||||
window.open(response.checkout_url, '_blank')
|
||||
}, reportError)
|
||||
|
||||
const accessBillingPortal = wrapWithErrorHandlingAsync(async () => {
|
||||
const response = await authStore.accessBillingPortal()
|
||||
if (!response.billing_portal_url) {
|
||||
throw new Error(
|
||||
t('toastMessages.failedToAccessBillingPortal', {
|
||||
error: 'No billing portal URL returned'
|
||||
})
|
||||
)
|
||||
}
|
||||
window.open(response.billing_portal_url, '_blank')
|
||||
}, reportError)
|
||||
|
||||
const fetchBalance = wrapWithErrorHandlingAsync(async () => {
|
||||
await authStore.fetchBalance()
|
||||
}, reportError)
|
||||
|
||||
return {
|
||||
logout,
|
||||
sendPasswordReset,
|
||||
purchaseCredits,
|
||||
accessBillingPortal,
|
||||
fetchBalance
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { $el } from '@/scripts/ui'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
@@ -113,7 +114,9 @@ export const useLitegraphService = () => {
|
||||
#addInputSocket(inputSpec: InputSpec) {
|
||||
const inputName = inputSpec.name
|
||||
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
|
||||
const widgetConstructor = widgetStore.widgets.get(inputSpec.type)
|
||||
const widgetConstructor = widgetStore.widgets.get(
|
||||
inputSpec.widgetType ?? inputSpec.type
|
||||
)
|
||||
if (widgetConstructor && !inputSpec.forceInput) return
|
||||
|
||||
this.addInput(inputName, inputSpec.type, {
|
||||
@@ -127,9 +130,13 @@ export const useLitegraphService = () => {
|
||||
* (unless `socketless`), an input socket is also added.
|
||||
*/
|
||||
#addInputWidget(inputSpec: InputSpec) {
|
||||
const widgetInputSpec = { ...inputSpec }
|
||||
if (inputSpec.widgetType) {
|
||||
widgetInputSpec.type = inputSpec.widgetType
|
||||
}
|
||||
const inputName = inputSpec.name
|
||||
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
|
||||
const widgetConstructor = widgetStore.widgets.get(inputSpec.type)
|
||||
const widgetConstructor = widgetStore.widgets.get(widgetInputSpec.type)
|
||||
if (!widgetConstructor || inputSpec.forceInput) return
|
||||
|
||||
const {
|
||||
@@ -139,7 +146,7 @@ export const useLitegraphService = () => {
|
||||
} = widgetConstructor(
|
||||
this,
|
||||
inputName,
|
||||
transformInputSpecV2ToV1(inputSpec),
|
||||
transformInputSpecV2ToV1(widgetInputSpec),
|
||||
app
|
||||
) ?? {}
|
||||
|
||||
@@ -153,7 +160,7 @@ export const useLitegraphService = () => {
|
||||
}
|
||||
|
||||
if (!widget?.options?.socketless) {
|
||||
const inputSpecV1 = transformInputSpecV2ToV1(inputSpec)
|
||||
const inputSpecV1 = transformInputSpecV2ToV1(widgetInputSpec)
|
||||
this.addInput(inputName, inputSpec.type, {
|
||||
shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined,
|
||||
localized_name: st(nameKey, inputName),
|
||||
@@ -208,7 +215,11 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setInitialSize() {
|
||||
const s = this.computeSize()
|
||||
s[0] = Math.max(this.#initialMinSize.width, s[0] * 1.5)
|
||||
// Expand the width a little to fit widget values on screen.
|
||||
const pad =
|
||||
this.widgets?.length &&
|
||||
!useSettingStore().get('LiteGraph.Node.DefaultPadding')
|
||||
s[0] = Math.max(this.#initialMinSize.width, s[0] + (pad ? 60 : 0))
|
||||
s[1] = Math.max(this.#initialMinSize.height, s[1])
|
||||
this.setSize(s)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import { COMFY_API_BASE_URL } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
import { useToastStore } from './toastStore'
|
||||
|
||||
type CreditPurchaseResponse =
|
||||
operations['InitiateCreditPurchase']['responses']['201']['content']['application/json']
|
||||
type CreditPurchasePayload =
|
||||
@@ -36,10 +34,16 @@ type AccessBillingPortalResponse =
|
||||
type AccessBillingPortalReqBody =
|
||||
operations['AccessBillingPortal']['requestBody']
|
||||
|
||||
export class FirebaseAuthStoreError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'FirebaseAuthStoreError'
|
||||
}
|
||||
}
|
||||
|
||||
export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const currentUser = ref<User | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
const customerCreated = ref(false)
|
||||
@@ -59,32 +63,21 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const userId = computed(() => currentUser.value?.uid)
|
||||
|
||||
// Get auth from VueFire and listen for auth state changes
|
||||
const auth = useFirebaseAuth()
|
||||
if (auth) {
|
||||
// Set persistence to localStorage (works in both browser and Electron)
|
||||
void setPersistence(auth, browserLocalPersistence)
|
||||
// From useFirebaseAuth docs:
|
||||
// Retrieves the Firebase Auth instance. Returns `null` on the server.
|
||||
// When using this function on the client in TypeScript, you can force the type with `useFirebaseAuth()!`.
|
||||
const auth = useFirebaseAuth()!
|
||||
// Set persistence to localStorage (works in both browser and Electron)
|
||||
void setPersistence(auth, browserLocalPersistence)
|
||||
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
currentUser.value = user
|
||||
isInitialized.value = true
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
currentUser.value = user
|
||||
isInitialized.value = true
|
||||
|
||||
// Reset balance when auth state changes
|
||||
balance.value = null
|
||||
lastBalanceUpdateTime.value = null
|
||||
})
|
||||
} else {
|
||||
error.value = 'Firebase Auth not available from VueFire'
|
||||
}
|
||||
|
||||
const showAuthErrorToast = () => {
|
||||
useToastStore().add({
|
||||
summary: t('g.error'),
|
||||
detail: t('auth.login.genericErrorMessage', {
|
||||
supportEmail: 'support@comfy.org'
|
||||
}),
|
||||
severity: 'error'
|
||||
})
|
||||
}
|
||||
// Reset balance when auth state changes
|
||||
balance.value = null
|
||||
lastBalanceUpdateTime.value = null
|
||||
})
|
||||
|
||||
const getIdToken = async (): Promise<string | null> => {
|
||||
if (currentUser.value) {
|
||||
@@ -98,9 +91,10 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
try {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
error.value = 'Cannot fetch balance: User not authenticated'
|
||||
isFetchingBalance.value = false
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/balance`, {
|
||||
@@ -115,8 +109,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return null
|
||||
}
|
||||
const errorData = await response.json()
|
||||
error.value = `Failed to fetch balance: ${errorData.message}`
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToFetchBalance', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const balanceData = await response.json()
|
||||
@@ -124,9 +121,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
lastBalanceUpdateTime.value = new Date()
|
||||
balance.value = balanceData
|
||||
return balanceData
|
||||
} catch (e) {
|
||||
error.value = `Failed to fetch balance: ${e}`
|
||||
return null
|
||||
} finally {
|
||||
isFetchingBalance.value = false
|
||||
}
|
||||
@@ -143,15 +137,21 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
}
|
||||
})
|
||||
if (!createCustomerRes.ok) {
|
||||
throw new Error(
|
||||
`Failed to create customer: ${createCustomerRes.statusText}`
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: createCustomerRes.statusText
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const createCustomerResJson: CreateCustomerResponse =
|
||||
await createCustomerRes.json()
|
||||
if (!createCustomerResJson?.id) {
|
||||
throw new Error('Failed to create customer: No customer ID returned')
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: 'No customer ID returned'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return createCustomerResJson
|
||||
@@ -163,10 +163,12 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
createCustomer?: boolean
|
||||
} = {}
|
||||
): Promise<T> => {
|
||||
if (!auth) throw new Error('Firebase Auth not initialized')
|
||||
if (!auth)
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.firebaseAuthNotInitialized')
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const result = await action(auth)
|
||||
@@ -181,10 +183,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Unknown error'
|
||||
showAuthErrorToast()
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -233,11 +231,10 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
const addCredits = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse | null> => {
|
||||
): Promise<CreditPurchaseResponse> => {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
error.value = 'Cannot add credits: User not authenticated'
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
// Ensure customer was created during login/registration
|
||||
@@ -257,9 +254,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
error.value = `Failed to initiate credit purchase: ${errorData.message}`
|
||||
showAuthErrorToast()
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToInitiateCreditPurchase', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
@@ -267,16 +266,15 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
const initiateCreditPurchase = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse | null> =>
|
||||
): Promise<CreditPurchaseResponse> =>
|
||||
executeAuthAction((_) => addCredits(requestBodyContent))
|
||||
|
||||
const accessBillingPortal = async (
|
||||
requestBody?: AccessBillingPortalReqBody
|
||||
): Promise<AccessBillingPortalResponse | null> => {
|
||||
): Promise<AccessBillingPortalResponse> => {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
error.value = 'Cannot access billing portal: User not authenticated'
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/billing`, {
|
||||
@@ -292,9 +290,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
error.value = `Failed to access billing portal: ${errorData.message}`
|
||||
showAuthErrorToast()
|
||||
return null
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToAccessBillingPortal', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
@@ -303,7 +303,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return {
|
||||
// State
|
||||
loading,
|
||||
error,
|
||||
currentUser,
|
||||
isInitialized,
|
||||
balance,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="comfyui-body grid h-screen w-screen overflow-hidden">
|
||||
<div class="comfyui-body grid h-full w-full overflow-hidden">
|
||||
<div id="comfyui-body-top" class="comfyui-body-top">
|
||||
<TopMenubar v-if="useNewMenu === 'Top'" />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<main class="w-full min-h-screen overflow-hidden relative">
|
||||
<main class="w-full h-full overflow-hidden relative">
|
||||
<router-view />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -135,7 +135,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
expect(store.userEmail).toBe('test@example.com')
|
||||
expect(store.userId).toBe('test-user-id')
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should set persistence to local storage on initialization', () => {
|
||||
@@ -158,34 +157,12 @@ describe('useFirebaseAuthStore', () => {
|
||||
// Error expected
|
||||
}
|
||||
|
||||
expect(store.error).toBe('Invalid password')
|
||||
|
||||
// Now, succeed on next attempt
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValueOnce({
|
||||
user: mockUser
|
||||
} as any)
|
||||
|
||||
await store.login('test@example.com', 'correct-password')
|
||||
|
||||
// Error should be cleared
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle auth initialization failure', async () => {
|
||||
// Mock auth as null to simulate initialization failure
|
||||
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(null)
|
||||
|
||||
// Create a new store instance
|
||||
setActivePinia(createPinia())
|
||||
const uninitializedStore = useFirebaseAuthStore()
|
||||
|
||||
// Check that isInitialized is false
|
||||
expect(uninitializedStore.isInitialized).toBe(false)
|
||||
|
||||
// Verify store actions throw appropriate errors
|
||||
await expect(
|
||||
uninitializedStore.login('test@example.com', 'password')
|
||||
).rejects.toThrow('Firebase Auth not initialized')
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
@@ -204,7 +181,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle login errors', async () => {
|
||||
@@ -223,7 +199,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
'wrong-password'
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Invalid password')
|
||||
})
|
||||
|
||||
it('should handle concurrent login attempts correctly', async () => {
|
||||
@@ -260,7 +235,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle registration errors', async () => {
|
||||
@@ -279,7 +253,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
'password'
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Email already in use')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -299,7 +272,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
await expect(store.logout()).rejects.toThrow('Network error')
|
||||
|
||||
expect(firebaseAuth.signOut).toHaveBeenCalledWith(mockAuth)
|
||||
expect(store.error).toBe('Network error')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -373,7 +345,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle Google sign in errors', async () => {
|
||||
@@ -389,7 +360,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
expect.any(firebaseAuth.GoogleAuthProvider)
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Google authentication failed')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -408,7 +378,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle Github sign in errors', async () => {
|
||||
@@ -424,7 +393,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
expect.any(firebaseAuth.GithubAuthProvider)
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Github authentication failed')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ export default defineConfig({
|
||||
comfyAPIPlugin(IS_DEV),
|
||||
generateImportMapPlugin([
|
||||
{ name: 'vue', pattern: /[\\/]node_modules[\\/]vue[\\/]/ },
|
||||
{ name: 'primevue', pattern: /[\\/]node_modules[\\/]primevue[\\/]/ }
|
||||
{ name: 'primevue', pattern: /[\\/]node_modules[\\/]primevue[\\/]/ },
|
||||
{ name: 'vue-i18n', pattern: /[\\/]node_modules[\\/]vue-i18n[\\/]/ }
|
||||
]),
|
||||
addElementVnodeExportPlugin(),
|
||||
|
||||
|
||||