Compare commits
70 Commits
menu-tests
...
model_file
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d61221ad3 | ||
|
|
fec5dbcf70 | ||
|
|
c72ba664ee | ||
|
|
6ed870d431 | ||
|
|
4e25a78d2d | ||
|
|
6408623b71 | ||
|
|
fdad2475ce | ||
|
|
5486fb94a0 | ||
|
|
34b1fd5a72 | ||
|
|
aa46524829 | ||
|
|
3bd87820eb | ||
|
|
0f95ed852e | ||
|
|
3501b480d4 | ||
|
|
5fa0401acd | ||
|
|
4d39dc28e0 | ||
|
|
2a297e512d | ||
|
|
2019c1d877 | ||
|
|
2425e32d51 | ||
|
|
b6466c44e5 | ||
|
|
13441add24 | ||
|
|
64ce8ce5ed | ||
|
|
841ff0b46c | ||
|
|
df86da3d67 | ||
|
|
3aea2c120a | ||
|
|
6fdef0308b | ||
|
|
973a1eb0a9 | ||
|
|
b9d9ce78f9 | ||
|
|
bb1ac32ccd | ||
|
|
1ef3c007e6 | ||
|
|
db81b62274 | ||
|
|
43365b4318 | ||
|
|
dfdffcd27e | ||
|
|
252454484f | ||
|
|
c1984f7ccd | ||
|
|
bbbf140b1f | ||
|
|
754eb807de | ||
|
|
fad6c6c502 | ||
|
|
926278e9ef | ||
|
|
131fae72d4 | ||
|
|
a8a7288e0d | ||
|
|
2a21dd1004 | ||
|
|
3bdd814b3c | ||
|
|
0da6be5cdc | ||
|
|
6a3cf9e698 | ||
|
|
6b98f1de0e | ||
|
|
d05a340949 | ||
|
|
ca16e55579 | ||
|
|
a54529da40 | ||
|
|
f52915a590 | ||
|
|
13b00cdbc6 | ||
|
|
626b54da27 | ||
|
|
c24472aeac | ||
|
|
ea605887fa | ||
|
|
6edea73bf8 | ||
|
|
6b8bf989f1 | ||
|
|
3758a435cb | ||
|
|
813bf11484 | ||
|
|
b5ae354bec | ||
|
|
7a1a626b36 | ||
|
|
63d24301a3 | ||
|
|
93d7d2c69c | ||
|
|
2d9a0d02ab | ||
|
|
6601cf6959 | ||
|
|
77ac4a415c | ||
|
|
8ae36e2c8d | ||
|
|
cb9ec354e7 | ||
|
|
8713cd6e25 | ||
|
|
c30244ef66 | ||
|
|
2024aa6562 | ||
|
|
00c2181bb6 |
@@ -18,3 +18,10 @@ TEST_COMFYUI_DIR=/home/ComfyUI
|
||||
|
||||
# Whether to enable minification of the frontend code.
|
||||
ENABLE_MINIFY=true
|
||||
|
||||
# Whether to disable proxying the `/templates` route. If true, allows you to
|
||||
# serve templates from the ComfyUI_frontend/public/templates folder (for
|
||||
# locally testing changes to templates). When false or nonexistent, the
|
||||
# templates are served via the normal method from the server's python site
|
||||
# packages.
|
||||
DISABLE_TEMPLATES_PROXY=false
|
||||
|
||||
443
README.md
@@ -67,6 +67,449 @@ 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>
|
||||
|
||||
## Contributing
|
||||
|
||||
We're building this frontend together and would love your help — no matter how you'd like to pitch in! You don't need to write code to make a difference.
|
||||
|
||||
@@ -33,5 +33,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -130,6 +130,11 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 2.1600300525920346,
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
63.071794466403446,
|
||||
75.18055335968394
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,6 +499,11 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
85
browser_tests/assets/execution/partial_execution.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"id": "1a95532f-c8aa-4c9d-a7f6-f928ba2d4862",
|
||||
"revision": 0,
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PreviewAny",
|
||||
"pos": [946.2566528320312, 598.4373168945312],
|
||||
"size": [140, 76],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PreviewAny",
|
||||
"pos": [951.0236206054688, 421.3861083984375],
|
||||
"size": [140, 76],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PrimitiveString",
|
||||
"pos": [575.1760864257812, 504.5214538574219],
|
||||
"size": [270, 58],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [2, 3]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveString"
|
||||
},
|
||||
"widgets_values": ["foo"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[2, 3, 0, 1, 0, "*"],
|
||||
[3, 3, 0, 4, 0, "*"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.19.1",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -160,4 +160,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"group_node": {
|
||||
"nodes": [
|
||||
@@ -401,4 +405,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"hello": {
|
||||
"nodes": [
|
||||
@@ -249,4 +253,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,11 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"groupNodes": {}
|
||||
"groupNodes": {},
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -92,10 +92,14 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": false,
|
||||
"VHS_KeepIntermediate": false
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -106,10 +106,7 @@
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": {
|
||||
"0": 0,
|
||||
"1": 0
|
||||
}
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
|
||||
@@ -368,10 +368,10 @@
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
149.9747408641311,
|
||||
383.8593224280729
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
"title": "Load Animated Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,11 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.17.0"
|
||||
"frontendVersion": "1.17.0",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
124
browser_tests/desktop/fixtures/electron.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
type ElectronFixtureOptions = {
|
||||
registerDefaults?: {
|
||||
downloadManager?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type MockFunction = {
|
||||
calls: unknown[][]
|
||||
called: () => Promise<void>
|
||||
handler?: (args: unknown[]) => unknown
|
||||
}
|
||||
|
||||
export type MockElectronAPI = {
|
||||
setup: (method: string, handler: (args: unknown[]) => unknown) => MockFunction
|
||||
}
|
||||
|
||||
export const electronFixture = base.extend<{
|
||||
electronAPI: MockElectronAPI
|
||||
electronOptions: ElectronFixtureOptions
|
||||
}>({
|
||||
electronOptions: [
|
||||
{
|
||||
registerDefaults: {
|
||||
downloadManager: true
|
||||
}
|
||||
},
|
||||
{ option: true }
|
||||
],
|
||||
|
||||
electronAPI: [
|
||||
async ({ page, electronOptions }, use) => {
|
||||
const mocks = new Map<string, MockFunction>()
|
||||
|
||||
await page.exposeFunction(
|
||||
'__handleMockCall',
|
||||
async (method: string, args: unknown[]) => {
|
||||
const mock = mocks.get(method)
|
||||
|
||||
if (electronOptions.registerDefaults?.downloadManager) {
|
||||
if (method === 'DownloadManager.getAllDownloads') {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (!mock) return null
|
||||
mock.calls.push(args)
|
||||
return mock.handler ? mock.handler(args) : null
|
||||
}
|
||||
)
|
||||
|
||||
const createMockFunction = (
|
||||
method: string,
|
||||
handler: (args: unknown[]) => unknown
|
||||
): MockFunction => {
|
||||
let resolveNextCall: (() => void) | null = null
|
||||
|
||||
const mockFn: MockFunction = {
|
||||
calls: [],
|
||||
async called() {
|
||||
if (this.calls.length > 0) return
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
resolveNextCall = resolve
|
||||
})
|
||||
},
|
||||
handler: (args: unknown[]) => {
|
||||
const result = handler(args)
|
||||
resolveNextCall?.()
|
||||
resolveNextCall = null
|
||||
return result
|
||||
}
|
||||
}
|
||||
mocks.set(method, mockFn)
|
||||
|
||||
// Add the method to the window.electronAPI object
|
||||
page.evaluate((methodName) => {
|
||||
const w = window as typeof window & {
|
||||
electronAPI: Record<string, any>
|
||||
}
|
||||
|
||||
w.electronAPI[methodName] = async (...args: unknown[]) => {
|
||||
return window['__handleMockCall'](methodName, args)
|
||||
}
|
||||
}, method)
|
||||
|
||||
return mockFn
|
||||
}
|
||||
|
||||
const testAPI: MockElectronAPI = {
|
||||
setup(method, handler) {
|
||||
console.log('adding handler for', method)
|
||||
return createMockFunction(method, handler)
|
||||
}
|
||||
}
|
||||
|
||||
await page.addInitScript(async () => {
|
||||
const getProxy = (...path: string[]) => {
|
||||
return new Proxy(() => {}, {
|
||||
// Handle the proxy itself being called as a function
|
||||
apply: async (target, thisArg, argArray) => {
|
||||
return window['__handleMockCall'](path.join('.'), argArray)
|
||||
},
|
||||
// Handle property access
|
||||
get: (target, prop: string) => {
|
||||
return getProxy(...path, prop)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const w = window as typeof window & {
|
||||
electronAPI: any
|
||||
}
|
||||
|
||||
w.electronAPI = getProxy()
|
||||
console.log('registered electron api')
|
||||
})
|
||||
|
||||
await use(testAPI)
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
})
|
||||
172
browser_tests/desktop/importModel.spec.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { ComfyPage, comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { MockElectronAPI, electronFixture } from './fixtures/electron'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, electronFixture)
|
||||
|
||||
comfyPageFixture.describe('Import Model (web)', () => {
|
||||
comfyPageFixture(
|
||||
'Import dialog does not show when electron api is not available',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.dragAndDropExternalResource({
|
||||
fileName: 'test.bin',
|
||||
buffer: Buffer.from('')
|
||||
})
|
||||
|
||||
// Normal unable to find workflow in file error
|
||||
await expect(
|
||||
comfyPage.page.locator('.p-toast-message.p-toast-message-warn')
|
||||
).toHaveCount(1)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Import Model (electron)', () => {
|
||||
const dropFile = async (
|
||||
comfyPage: ComfyPage,
|
||||
electronAPI: MockElectronAPI,
|
||||
fileName: string,
|
||||
metadata: string
|
||||
) => {
|
||||
const getFilePathMock = electronAPI.setup('getFilePath', () =>
|
||||
Promise.resolve('some/file/path/' + fileName)
|
||||
)
|
||||
|
||||
let buffer: Buffer | undefined
|
||||
if (metadata) {
|
||||
const contentBuffer = Buffer.from(metadata, 'utf-8')
|
||||
|
||||
const headerSizeBuffer = Buffer.alloc(8)
|
||||
headerSizeBuffer.writeBigUInt64LE(BigInt(contentBuffer.length))
|
||||
|
||||
buffer = Buffer.concat([headerSizeBuffer, contentBuffer])
|
||||
}
|
||||
|
||||
await comfyPage.dragAndDropExternalResource({
|
||||
fileName,
|
||||
buffer
|
||||
})
|
||||
await getFilePathMock.called()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('.p-toast-message.p-toast-message-warn')
|
||||
).toHaveCount(0)
|
||||
await expect(comfyPage.importModelDialog.rootEl).toBeVisible()
|
||||
}
|
||||
|
||||
test('Can show import file dialog by dropping file onto the app', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(comfyPage, electronAPI, 'test.bin', '{}')
|
||||
})
|
||||
|
||||
test('Can autodetect checkpoint model type from modelspec', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
__metadata__: {
|
||||
'modelspec.sai_model_spec': 'test',
|
||||
'modelspec.architecture': 'stable-diffusion-v1'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'checkpoints'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can autodetect lora model type from modelspec', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
__metadata__: {
|
||||
'modelspec.sai_model_spec': 'test',
|
||||
'modelspec.architecture': 'Flux.1-AE/lora'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'loras'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can autodetect checkpoint model type from header keys', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
'model.diffusion_model.input_blocks.0.0.bias': {}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'checkpoints'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can autodetect lora model type from header keys', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
'lora_unet_down_blocks_0_attentions_0_proj_in.alpha': {}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'loras'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can import file', async ({ comfyPage, electronAPI }) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'checkpoint_modelspec.safetensors',
|
||||
'{}'
|
||||
)
|
||||
|
||||
const importModelMock = electronAPI.setup(
|
||||
'importModel',
|
||||
() => new Promise((resolve) => setTimeout(resolve, 100))
|
||||
)
|
||||
|
||||
// Model type is required so select one
|
||||
await expect(comfyPage.importModelDialog.importButton).toBeDisabled()
|
||||
await comfyPage.importModelDialog.modelTypeInput.fill('checkpoints')
|
||||
await expect(comfyPage.importModelDialog.importButton).toBeEnabled()
|
||||
|
||||
// Click import, ensure API is called
|
||||
await comfyPage.importModelDialog.importButton.click()
|
||||
await importModelMock.called()
|
||||
|
||||
// Toast should be shown and dialog closes
|
||||
await expect(
|
||||
comfyPage.page.locator('.p-toast-message.p-toast-message-success')
|
||||
).toHaveCount(1)
|
||||
|
||||
await expect(comfyPage.importModelDialog.rootEl).toBeHidden()
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import { ComfyActionbar } from '../helpers/actionbar'
|
||||
import { ComfyTemplates } from '../helpers/templates'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { ImportModelDialog } from './components/ImportModelDialog'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
NodeLibrarySidebarTab,
|
||||
@@ -140,6 +141,7 @@ export class ComfyPage {
|
||||
public readonly templates: ComfyTemplates
|
||||
public readonly settingDialog: SettingDialog
|
||||
public readonly confirmDialog: ConfirmDialog
|
||||
public readonly importModelDialog: ImportModelDialog
|
||||
|
||||
/** Worker index to test user ID */
|
||||
public readonly userIds: string[] = []
|
||||
@@ -165,6 +167,7 @@ export class ComfyPage {
|
||||
this.templates = new ComfyTemplates(page)
|
||||
this.settingDialog = new SettingDialog(page)
|
||||
this.confirmDialog = new ConfirmDialog(page)
|
||||
this.importModelDialog = new ImportModelDialog(page)
|
||||
}
|
||||
|
||||
convertLeafToContent(structure: FolderStructure): FolderStructure {
|
||||
@@ -270,6 +273,7 @@ export class ComfyPage {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
localStorage.setItem('api-nodes-news-seen', 'true')
|
||||
}, this.id)
|
||||
}
|
||||
await this.goto()
|
||||
@@ -468,6 +472,7 @@ export class ComfyPage {
|
||||
fileName?: string
|
||||
url?: string
|
||||
dropPosition?: Position
|
||||
buffer?: Buffer
|
||||
} = {}
|
||||
) {
|
||||
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
|
||||
@@ -486,7 +491,7 @@ export class ComfyPage {
|
||||
// Dropping a file from the filesystem
|
||||
if (fileName) {
|
||||
const filePath = this.assetPath(fileName)
|
||||
const buffer = fs.readFileSync(filePath)
|
||||
const buffer = options.buffer ?? fs.readFileSync(filePath)
|
||||
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
|
||||
17
browser_tests/fixtures/components/ImportModelDialog.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Page } from '@playwright/test'
|
||||
|
||||
export class ImportModelDialog {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
get rootEl() {
|
||||
return this.page.locator('div[aria-labelledby="global-import-model"]')
|
||||
}
|
||||
|
||||
get modelTypeInput() {
|
||||
return this.rootEl.locator('#model-type')
|
||||
}
|
||||
|
||||
get importButton() {
|
||||
return this.rootEl.getByLabel('Import')
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
@@ -18,3 +18,27 @@ test.describe('Execution', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Execute to selected output nodes', () => {
|
||||
test('Execute to selected output nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution/partial_execution')
|
||||
const input = await comfyPage.getNodeRefById(3)
|
||||
const output1 = await comfyPage.getNodeRefById(1)
|
||||
const output2 = await comfyPage.getNodeRefById(4)
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
|
||||
await output1.click('title')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
||||
// @note: Wait for the execution to finish. We might want to move to a more
|
||||
// reliable way to wait for the execution to finish. Workflow in this test
|
||||
// is simple enough that this is fine for now.
|
||||
await comfyPage.page.waitForTimeout(200)
|
||||
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
@@ -666,6 +666,12 @@ test.describe('Load workflow', () => {
|
||||
expect(activeWorkflowName).toEqual(workflowPathB)
|
||||
})
|
||||
})
|
||||
|
||||
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false)
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load duplicate workflow', () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |