Compare commits
1 Commits
model_file
...
export-gen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6a98e3286 |
@@ -18,10 +18,3 @@ 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
|
||||
|
||||
37
.github/copilot-instructions.md
vendored
@@ -1,37 +0,0 @@
|
||||
Use the Vue 3 Composition API instead of the Options API when writing Vue components. An exception is when overriding or extending a PrimeVue component for compatibility, you may use the Options API.
|
||||
|
||||
Use setup() function for component logic
|
||||
|
||||
Utilize ref and reactive for reactive state
|
||||
|
||||
Implement computed properties with computed()
|
||||
|
||||
Use watch and watchEffect for side effects
|
||||
|
||||
Implement lifecycle hooks with onMounted, onUpdated, etc.
|
||||
|
||||
Utilize provide/inject for dependency injection
|
||||
|
||||
Use vue 3.5 style of default prop declaration.
|
||||
|
||||
Use Tailwind CSS for styling
|
||||
|
||||
Leverage VueUse functions for performance-enhancing styles
|
||||
|
||||
Use lodash for utility functions
|
||||
|
||||
Use TypeScript for type safety
|
||||
|
||||
Implement proper props and emits definitions
|
||||
|
||||
Utilize Vue 3's Teleport component when needed
|
||||
|
||||
Use Suspense for async components
|
||||
|
||||
Implement proper error handling
|
||||
|
||||
Follow Vue 3 style guide and naming conventions
|
||||
|
||||
Use Vite for fast development and building
|
||||
|
||||
Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json.
|
||||
2
.github/workflows/release.yaml
vendored
@@ -29,9 +29,9 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
npm ci
|
||||
npm run fetch-templates
|
||||
npm run build
|
||||
npm run zipdist
|
||||
- name: Upload dist artifact
|
||||
|
||||
3
.github/workflows/test-ui.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||
ref: '9d2421fd3208a310e4d0f71fca2ea0c985759c33'
|
||||
ref: '49c8220be49120dbaff85f32813d854d6dff2d05'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -39,6 +39,7 @@ jobs:
|
||||
- name: Build ComfyUI_frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run fetch-templates
|
||||
npm run build
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
|
||||
2
.github/workflows/update-electron-types.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
title: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
body: |
|
||||
|
||||
3
.gitignore
vendored
@@ -55,6 +55,3 @@ dist.zip
|
||||
|
||||
# Temporary repository directory
|
||||
templates_repo/
|
||||
|
||||
# Vite’s timestamped config modules
|
||||
vite.config.mts.timestamp-*.mjs
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
|
||||
module.exports = defineConfig({
|
||||
modelName: 'gpt-4.1',
|
||||
modelName: 'gpt-4',
|
||||
splitToken: 1024,
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
`
|
||||
|
||||
20
README.md
@@ -510,20 +510,6 @@ 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.
|
||||
|
||||
Here are some ways to get involved:
|
||||
|
||||
- **Pull Requests:** Add features, fix bugs, or improve code health. Browse [issues](https://github.com/Comfy-Org/ComfyUI_frontend/issues) for inspiration.
|
||||
- **Vote on Features:** Give a 👍 to the feature requests you care about to help us prioritize.
|
||||
- **Verify Bugs:** Try reproducing reported issues and share your results (even if the bug doesn't occur!).
|
||||
- **Community Support:** Hop into our [Discord](https://www.comfy.org/discord) to answer questions or get help.
|
||||
- **Share & Advocate:** Tell your friends, tweet about us, or share tips to support the project.
|
||||
|
||||
Have another idea? Drop into Discord or open an issue, and let's chat!
|
||||
|
||||
## Development
|
||||
|
||||
### Tech Stack
|
||||
@@ -571,12 +557,6 @@ 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: 16 KiB |
@@ -33,11 +33,5 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -130,11 +130,6 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"scale": 2.1600300525920346,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
63.071794466403446,
|
||||
75.18055335968394
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -499,11 +499,6 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
{
|
||||
"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,10 +43,6 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"group_node": {
|
||||
"nodes": [
|
||||
@@ -405,4 +401,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -110,10 +110,6 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"hello": {
|
||||
"nodes": [
|
||||
@@ -253,4 +249,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -44,11 +44,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -61,11 +61,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -50,11 +50,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,7 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"groupNodes": {},
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
"groupNodes": {}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -54,11 +54,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -92,14 +92,10 @@
|
||||
"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,10 +84,6 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -106,7 +106,10 @@
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
"offset": {
|
||||
"0": 0,
|
||||
"1": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
|
||||
@@ -368,10 +368,10 @@
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
149.9747408641311,
|
||||
383.8593224280729
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,5 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"8": {
|
||||
"inputs": {
|
||||
"image": "animated_web.webp"
|
||||
},
|
||||
"class_type": "DevToolsLoadAnimatedImageTest",
|
||||
"_meta": {
|
||||
"title": "Load Animated Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -41,11 +41,6 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"id": "3f1fcbf9-f9de-4935-8fad-401813f61b13",
|
||||
"revision": 0,
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveAnimatedWEBP",
|
||||
"pos": [336, 104],
|
||||
"size": [210, 368],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI", 6, true, 80, "default"]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "DevToolsLoadAnimatedImageTest",
|
||||
"pos": [64, 104],
|
||||
"size": [210, 316],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [4]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsLoadAnimatedImageTest"
|
||||
},
|
||||
"widgets_values": ["animated_web.webp", "image"]
|
||||
}
|
||||
],
|
||||
"links": [[4, 10, 0, 9, 0, "IMAGE"]],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.17.0",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
Before Width: | Height: | Size: 75 KiB |
@@ -1,124 +0,0 @@
|
||||
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 }
|
||||
]
|
||||
})
|
||||
@@ -1,172 +0,0 @@
|
||||
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,7 +13,6 @@ 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,
|
||||
@@ -141,7 +140,6 @@ 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[] = []
|
||||
@@ -167,7 +165,6 @@ 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 {
|
||||
@@ -273,7 +270,6 @@ export class ComfyPage {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
localStorage.setItem('api-nodes-news-seen', 'true')
|
||||
}, this.id)
|
||||
}
|
||||
await this.goto()
|
||||
@@ -467,128 +463,85 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropExternalResource(
|
||||
options: {
|
||||
fileName?: string
|
||||
url?: string
|
||||
dropPosition?: Position
|
||||
buffer?: Buffer
|
||||
} = {}
|
||||
) {
|
||||
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
|
||||
|
||||
if (!fileName && !url)
|
||||
throw new Error('Must provide either fileName or url')
|
||||
|
||||
const evaluateParams: {
|
||||
dropPosition: Position
|
||||
fileName?: string
|
||||
fileType?: string
|
||||
buffer?: Uint8Array | number[]
|
||||
url?: string
|
||||
} = { dropPosition }
|
||||
|
||||
// Dropping a file from the filesystem
|
||||
if (fileName) {
|
||||
const filePath = this.assetPath(fileName)
|
||||
const buffer = options.buffer ?? fs.readFileSync(filePath)
|
||||
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
if (fileName.endsWith('.svg')) return 'image/svg+xml'
|
||||
if (fileName.endsWith('.webp')) return 'image/webp'
|
||||
if (fileName.endsWith('.webm')) return 'video/webm'
|
||||
if (fileName.endsWith('.json')) return 'application/json'
|
||||
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
||||
evaluateParams.fileName = fileName
|
||||
evaluateParams.fileType = getFileType(fileName)
|
||||
evaluateParams.buffer = [...new Uint8Array(buffer)]
|
||||
}
|
||||
|
||||
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
|
||||
if (url) evaluateParams.url = url
|
||||
|
||||
// Execute the drag and drop in the browser
|
||||
await this.page.evaluate(async (params) => {
|
||||
const dataTransfer = new DataTransfer()
|
||||
|
||||
// Add file if provided
|
||||
if (params.buffer && params.fileName && params.fileType) {
|
||||
const file = new File(
|
||||
[new Uint8Array(params.buffer)],
|
||||
params.fileName,
|
||||
{
|
||||
type: params.fileType
|
||||
}
|
||||
)
|
||||
dataTransfer.items.add(file)
|
||||
}
|
||||
|
||||
// Add URL data if provided
|
||||
if (params.url) {
|
||||
dataTransfer.setData('text/uri-list', params.url)
|
||||
dataTransfer.setData('text/x-moz-url', params.url)
|
||||
}
|
||||
|
||||
const targetElement = document.elementFromPoint(
|
||||
params.dropPosition.x,
|
||||
params.dropPosition.y
|
||||
)
|
||||
|
||||
if (!targetElement) {
|
||||
console.error('No element found at drop position:', params.dropPosition)
|
||||
return { success: false, error: 'No element at position' }
|
||||
}
|
||||
|
||||
const eventOptions = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer,
|
||||
clientX: params.dropPosition.x,
|
||||
clientY: params.dropPosition.y
|
||||
}
|
||||
|
||||
const dragOverEvent = new DragEvent('dragover', eventOptions)
|
||||
const dropEvent = new DragEvent('drop', eventOptions)
|
||||
|
||||
Object.defineProperty(dropEvent, 'preventDefault', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
Object.defineProperty(dropEvent, 'stopPropagation', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
targetElement.dispatchEvent(dragOverEvent)
|
||||
targetElement.dispatchEvent(dropEvent)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
targetInfo: {
|
||||
tagName: targetElement.tagName,
|
||||
id: targetElement.id,
|
||||
classList: Array.from(targetElement.classList)
|
||||
}
|
||||
}
|
||||
}, evaluateParams)
|
||||
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragAndDropFile(
|
||||
fileName: string,
|
||||
options: { dropPosition?: Position } = {}
|
||||
options: {
|
||||
dropPosition?: Position
|
||||
} = {}
|
||||
) {
|
||||
return this.dragAndDropExternalResource({ fileName, ...options })
|
||||
}
|
||||
const { dropPosition = { x: 100, y: 100 } } = options
|
||||
|
||||
async dragAndDropURL(url: string, options: { dropPosition?: Position } = {}) {
|
||||
return this.dragAndDropExternalResource({ url, ...options })
|
||||
const filePath = this.assetPath(fileName)
|
||||
|
||||
// Read the file content
|
||||
const buffer = fs.readFileSync(filePath)
|
||||
|
||||
// Get file type
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
if (fileName.endsWith('.webp')) return 'image/webp'
|
||||
if (fileName.endsWith('.webm')) return 'video/webm'
|
||||
if (fileName.endsWith('.json')) return 'application/json'
|
||||
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
|
||||
const fileType = getFileType(fileName)
|
||||
|
||||
await this.page.evaluate(
|
||||
async ({ buffer, fileName, fileType, dropPosition }) => {
|
||||
const file = new File([new Uint8Array(buffer)], fileName, {
|
||||
type: fileType
|
||||
})
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.items.add(file)
|
||||
|
||||
const targetElement = document.elementFromPoint(
|
||||
dropPosition.x,
|
||||
dropPosition.y
|
||||
)
|
||||
|
||||
if (!targetElement) {
|
||||
console.error('No element found at drop position:', dropPosition)
|
||||
return { success: false, error: 'No element at position' }
|
||||
}
|
||||
|
||||
const eventOptions = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer,
|
||||
clientX: dropPosition.x,
|
||||
clientY: dropPosition.y
|
||||
}
|
||||
|
||||
const dragOverEvent = new DragEvent('dragover', eventOptions)
|
||||
const dropEvent = new DragEvent('drop', eventOptions)
|
||||
|
||||
Object.defineProperty(dropEvent, 'preventDefault', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
Object.defineProperty(dropEvent, 'stopPropagation', {
|
||||
value: () => {},
|
||||
writable: false
|
||||
})
|
||||
|
||||
targetElement.dispatchEvent(dragOverEvent)
|
||||
targetElement.dispatchEvent(dropEvent)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
targetInfo: {
|
||||
tagName: targetElement.tagName,
|
||||
id: targetElement.id,
|
||||
classList: Array.from(targetElement.classList)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ buffer: [...new Uint8Array(buffer)], fileName, fileType, dropPosition }
|
||||
)
|
||||
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async dragNode2() {
|
||||
@@ -929,16 +882,10 @@ export class ComfyPage {
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas(pos)
|
||||
}, pos)
|
||||
}
|
||||
|
||||
/** Get number of DOM widgets on the canvas. */
|
||||
async getDOMWidgetCount() {
|
||||
return await this.page.locator('.dom-widget').count()
|
||||
}
|
||||
|
||||
async getNodeRefById(id: NodeId) {
|
||||
return new NodeReference(id, this)
|
||||
}
|
||||
async getNodes(): Promise<LGraphNode[]> {
|
||||
async getNodes() {
|
||||
return await this.page.evaluate(() => {
|
||||
return window['app'].graph.nodes
|
||||
})
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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')
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ const customColorPalettes: Record<string, Palette> = {
|
||||
WIDGET_OUTLINE_COLOR: '#333',
|
||||
WIDGET_TEXT_COLOR: '#a3a3a8',
|
||||
WIDGET_SECONDARY_TEXT_COLOR: '#97979c',
|
||||
WIDGET_DISABLED_TEXT_COLOR: '#646464',
|
||||
LINK_COLOR: '#9A9',
|
||||
EVENT_LINK_COLOR: '#A86',
|
||||
CONNECTING_LINK_COLOR: '#AFA'
|
||||
@@ -112,7 +111,6 @@ const customColorPalettes: Record<string, Palette> = {
|
||||
WIDGET_OUTLINE_COLOR: '#333',
|
||||
WIDGET_TEXT_COLOR: '#a3a3a8',
|
||||
WIDGET_SECONDARY_TEXT_COLOR: '#97979c',
|
||||
WIDGET_DISABLED_TEXT_COLOR: '#646464',
|
||||
LINK_COLOR: '#9A9',
|
||||
EVENT_LINK_COLOR: '#A86',
|
||||
CONNECTING_LINK_COLOR: '#AFA'
|
||||
|
||||
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 99 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: 159 KiB After Width: | Height: | Size: 160 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: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
@@ -341,30 +341,3 @@ test.describe('Error dialog', () => {
|
||||
await expect(errorDialog).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Signin dialog', () => {
|
||||
test('Paste content to signin dialog should not paste node on canvas', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodeNum = (await comfyPage.getNodes()).length
|
||||
await comfyPage.clickEmptyLatentNode()
|
||||
await comfyPage.ctrlC()
|
||||
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
await textBox.click()
|
||||
await textBox.fill('test_password')
|
||||
await textBox.press('Control+a')
|
||||
await textBox.press('Control+c')
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].extensionManager.dialog.showSignInDialog()
|
||||
})
|
||||
|
||||
const input = comfyPage.page.locator('#comfy-org-sign-in-password')
|
||||
await input.waitFor({ state: 'visible' })
|
||||
await input.press('Control+v')
|
||||
await expect(input).toHaveValue('test_password')
|
||||
|
||||
expect(await comfyPage.getNodes()).toHaveLength(nodeNum)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,27 +24,4 @@ test.describe('DOM Widget', () => {
|
||||
await expect(firstMultiline).not.toBeVisible()
|
||||
await expect(lastMultiline).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Position update when entering focus mode', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
|
||||
})
|
||||
|
||||
// No DOM widget should be created by creation of interim LGraphNode objects.
|
||||
test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => {
|
||||
const initialCount = await comfyPage.getDOMWidgetCount()
|
||||
|
||||
// TextEncodeNode1
|
||||
await comfyPage.page.mouse.move(618, 191)
|
||||
await comfyPage.page.keyboard.down('Alt')
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(100, 100)
|
||||
await comfyPage.page.mouse.up()
|
||||
await comfyPage.page.keyboard.up('Alt')
|
||||
|
||||
const finalCount = await comfyPage.getDOMWidgetCount()
|
||||
expect(finalCount).toBe(initialCount + 1)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 83 KiB |
@@ -18,27 +18,3 @@ 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: 99 KiB After Width: | Height: | Size: 100 KiB |
@@ -666,12 +666,6 @@ 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: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 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: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 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: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 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 |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 80 KiB |