Merge pull request #102 from space-nuko/mobile-improvements

Mobile improvements
This commit is contained in:
space-nuko
2023-05-31 22:38:50 -05:00
committed by GitHub
34 changed files with 1394 additions and 519 deletions

View File

@@ -7,17 +7,6 @@
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#2196f3"> <meta name="theme-color" content="#2196f3">
</head> </head>
<script>
if(!window.location.search.substring(1) == "desktop=true") {
if (navigator.userAgent.match(/iPhone/i)
|| navigator.userAgent.match(/iPad/i)
|| navigator.userAgent.match(/Android/i)
|| navigator.userAgent.match(/Blackberry/i)
|| navigator.userAgent.match(/WebOs/i)) {
window.location.href = "/mobile/"
}
}
</script>
<body> <body>
<div id="app-root"/> <div id="app-root"/>
<script type="module" src='/src/main-desktop.ts'></script> <script type="module" src='/src/main-desktop.ts'></script>

View File

@@ -30,7 +30,7 @@
"prettier-plugin-svelte": "^2.10.0", "prettier-plugin-svelte": "^2.10.0",
"rollup-plugin-visualizer": "^5.9.0", "rollup-plugin-visualizer": "^5.9.0",
"sass": "^1.61.0", "sass": "^1.61.0",
"svelte": "^3.58.0", "svelte": "^3.59.0",
"svelte-check": "^3.2.0", "svelte-check": "^3.2.0",
"svelte-dnd-action": "^0.9.22", "svelte-dnd-action": "^0.9.22",
"typescript": "^5.0.3", "typescript": "^5.0.3",

81
pnpm-lock.yaml generated
View File

@@ -27,7 +27,7 @@ importers:
version: 6.11.0 version: 6.11.0
'@dogagenc/svelte-markdown': '@dogagenc/svelte-markdown':
specifier: ^0.2.4 specifier: ^0.2.4
version: 0.2.4(svelte@3.58.0) version: 0.2.4(svelte@3.59.1)
'@gradio/accordion': '@gradio/accordion':
specifier: workspace:* specifier: workspace:*
version: link:gradio/js/accordion version: link:gradio/js/accordion
@@ -93,7 +93,7 @@ importers:
version: link:litegraph/packages/tsconfig version: link:litegraph/packages/tsconfig
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1(svelte@3.58.0)(vite@4.3.8) version: 2.1.1(svelte@3.59.1)(vite@4.3.8)
'@tsconfig/svelte': '@tsconfig/svelte':
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1 version: 4.0.1
@@ -156,13 +156,13 @@ importers:
version: 1.5.2 version: 1.5.2
svelte-preprocess: svelte-preprocess:
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3) version: 5.0.3(sass@1.61.0)(svelte@3.59.1)(typescript@5.0.3)
svelte-select: svelte-select:
specifier: ^5.5.3 specifier: ^5.5.3
version: 5.5.3 version: 5.5.3
svelte-splitpanes: svelte-splitpanes:
specifier: ^0.7.13 specifier: ^0.7.13
version: 0.7.13(svelte@3.58.0) version: 0.7.13(svelte@3.59.1)
svelte-tiny-virtual-list: svelte-tiny-virtual-list:
specifier: ^2.0.5 specifier: ^2.0.5
version: 2.0.5 version: 2.0.5
@@ -190,7 +190,7 @@ importers:
version: 1.2.8 version: 1.2.8
'@zerodevx/svelte-toast': '@zerodevx/svelte-toast':
specifier: ^0.9.3 specifier: ^0.9.3
version: 0.9.3(svelte@3.58.0) version: 0.9.3(svelte@3.59.1)
eslint: eslint:
specifier: ^8.37.0 specifier: ^8.37.0
version: 8.37.0 version: 8.37.0
@@ -199,7 +199,7 @@ importers:
version: 8.8.0(eslint@8.37.0) version: 8.8.0(eslint@8.37.0)
eslint-plugin-svelte3: eslint-plugin-svelte3:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0(eslint@8.37.0)(svelte@3.58.0) version: 4.0.0(eslint@8.37.0)(svelte@3.59.1)
happy-dom: happy-dom:
specifier: ^9.18.3 specifier: ^9.18.3
version: 9.18.3 version: 9.18.3
@@ -211,7 +211,7 @@ importers:
version: 2.8.7 version: 2.8.7
prettier-plugin-svelte: prettier-plugin-svelte:
specifier: ^2.10.0 specifier: ^2.10.0
version: 2.10.0(prettier@2.8.7)(svelte@3.58.0) version: 2.10.0(prettier@2.8.7)(svelte@3.59.1)
rollup-plugin-visualizer: rollup-plugin-visualizer:
specifier: ^5.9.0 specifier: ^5.9.0
version: 5.9.0 version: 5.9.0
@@ -219,14 +219,14 @@ importers:
specifier: ^1.61.0 specifier: ^1.61.0
version: 1.61.0 version: 1.61.0
svelte: svelte:
specifier: ^3.58.0 specifier: ^3.59.0
version: 3.58.0 version: 3.59.1
svelte-check: svelte-check:
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.0(sass@1.61.0)(svelte@3.58.0) version: 3.2.0(sass@1.61.0)(svelte@3.59.1)
svelte-dnd-action: svelte-dnd-action:
specifier: ^0.9.22 specifier: ^0.9.22
version: 0.9.22(svelte@3.58.0) version: 0.9.22(svelte@3.59.1)
typescript: typescript:
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3 version: 5.0.3
@@ -1363,6 +1363,7 @@ packages:
'@codemirror/language': ^6.0.0 '@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0 '@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0 '@codemirror/view': ^6.0.0
'@lezer/common': ^1.0.0
dependencies: dependencies:
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/state': 6.2.0 '@codemirror/state': 6.2.0
@@ -1506,14 +1507,14 @@ packages:
w3c-keyname: 2.2.6 w3c-keyname: 2.2.6
dev: false dev: false
/@dogagenc/svelte-markdown@0.2.4(svelte@3.58.0): /@dogagenc/svelte-markdown@0.2.4(svelte@3.59.1):
resolution: {integrity: sha512-UmmHHZ7rilAbBYiNsxuL5d8Ac79EhFXrhjsUNr30BPzn+T7ohJR8kHMFjDYDQc0tOQOfKbICvkPAQ6cprqS3Eg==} resolution: {integrity: sha512-UmmHHZ7rilAbBYiNsxuL5d8Ac79EhFXrhjsUNr30BPzn+T7ohJR8kHMFjDYDQc0tOQOfKbICvkPAQ6cprqS3Eg==}
peerDependencies: peerDependencies:
svelte: ^3.0.0 svelte: ^3.0.0
dependencies: dependencies:
'@types/marked': 4.3.1 '@types/marked': 4.3.1
marked: 4.3.0 marked: 4.3.0
svelte: 3.58.0 svelte: 3.59.1
dev: false dev: false
/@esbuild/android-arm64@0.17.18: /@esbuild/android-arm64@0.17.18:
@@ -2275,7 +2276,7 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@sveltejs/vite-plugin-svelte@2.1.1(svelte@3.58.0)(vite@4.3.8): /@sveltejs/vite-plugin-svelte@2.1.1(svelte@3.59.1)(vite@4.3.8):
resolution: {integrity: sha512-7YeBDt4us0FiIMNsVXxyaP4Hwyn2/v9x3oqStkHU3ZdIc5O22pGwUwH33wUqYo+7Itdmo8zxJ45Qvfm3H7UUjQ==} resolution: {integrity: sha512-7YeBDt4us0FiIMNsVXxyaP4Hwyn2/v9x3oqStkHU3ZdIc5O22pGwUwH33wUqYo+7Itdmo8zxJ45Qvfm3H7UUjQ==}
engines: {node: ^14.18.0 || >= 16} engines: {node: ^14.18.0 || >= 16}
peerDependencies: peerDependencies:
@@ -2286,8 +2287,8 @@ packages:
deepmerge: 4.3.1 deepmerge: 4.3.1
kleur: 4.1.5 kleur: 4.1.5
magic-string: 0.30.0 magic-string: 0.30.0
svelte: 3.58.0 svelte: 3.59.1
svelte-hmr: 0.15.1(svelte@3.58.0) svelte-hmr: 0.15.1(svelte@3.59.1)
vite: 4.3.8(sass@1.61.0) vite: 4.3.8(sass@1.61.0)
vitefu: 0.2.4(vite@4.3.8) vitefu: 0.2.4(vite@4.3.8)
transitivePeerDependencies: transitivePeerDependencies:
@@ -2604,12 +2605,12 @@ packages:
pretty-format: 27.5.1 pretty-format: 27.5.1
dev: false dev: false
/@zerodevx/svelte-toast@0.9.3(svelte@3.58.0): /@zerodevx/svelte-toast@0.9.3(svelte@3.59.1):
resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==} resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==}
peerDependencies: peerDependencies:
svelte: ^3.57.0 svelte: ^3.57.0
dependencies: dependencies:
svelte: 3.58.0 svelte: 3.59.1
dev: true dev: true
/abab@2.0.6: /abab@2.0.6:
@@ -3117,6 +3118,8 @@ packages:
'@codemirror/search': 6.4.0 '@codemirror/search': 6.4.0
'@codemirror/state': 6.2.0 '@codemirror/state': 6.2.0
'@codemirror/view': 6.11.0 '@codemirror/view': 6.11.0
transitivePeerDependencies:
- '@lezer/common'
dev: false dev: false
/codemirror@6.0.1(@lezer/common@1.0.2): /codemirror@6.0.1(@lezer/common@1.0.2):
@@ -3941,14 +3944,14 @@ packages:
- typescript - typescript
dev: true dev: true
/eslint-plugin-svelte3@4.0.0(eslint@8.37.0)(svelte@3.58.0): /eslint-plugin-svelte3@4.0.0(eslint@8.37.0)(svelte@3.59.1):
resolution: {integrity: sha512-OIx9lgaNzD02+MDFNLw0GEUbuovNcglg+wnd/UY0fbZmlQSz7GlQiQ1f+yX0XvC07XPcDOnFcichqI3xCwp71g==} resolution: {integrity: sha512-OIx9lgaNzD02+MDFNLw0GEUbuovNcglg+wnd/UY0fbZmlQSz7GlQiQ1f+yX0XvC07XPcDOnFcichqI3xCwp71g==}
peerDependencies: peerDependencies:
eslint: '>=8.0.0' eslint: '>=8.0.0'
svelte: ^3.2.0 svelte: ^3.2.0
dependencies: dependencies:
eslint: 8.37.0 eslint: 8.37.0
svelte: 3.58.0 svelte: 3.59.1
dev: true dev: true
/eslint-scope@5.1.1: /eslint-scope@5.1.1:
@@ -5988,14 +5991,14 @@ packages:
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
dev: true dev: true
/prettier-plugin-svelte@2.10.0(prettier@2.8.7)(svelte@3.58.0): /prettier-plugin-svelte@2.10.0(prettier@2.8.7)(svelte@3.59.1):
resolution: {integrity: sha512-GXMY6t86thctyCvQq+jqElO+MKdB09BkL3hexyGP3Oi8XLKRFaJP1ud/xlWCZ9ZIa2BxHka32zhHfcuU+XsRQg==} resolution: {integrity: sha512-GXMY6t86thctyCvQq+jqElO+MKdB09BkL3hexyGP3Oi8XLKRFaJP1ud/xlWCZ9ZIa2BxHka32zhHfcuU+XsRQg==}
peerDependencies: peerDependencies:
prettier: ^1.16.4 || ^2.0.0 prettier: ^1.16.4 || ^2.0.0
svelte: ^3.2.0 svelte: ^3.2.0
dependencies: dependencies:
prettier: 2.8.7 prettier: 2.8.7
svelte: 3.58.0 svelte: 3.59.1
dev: true dev: true
/prettier@2.8.7: /prettier@2.8.7:
@@ -6656,7 +6659,7 @@ packages:
- sugarss - sugarss
dev: true dev: true
/svelte-check@3.2.0(sass@1.61.0)(svelte@3.58.0): /svelte-check@3.2.0(sass@1.61.0)(svelte@3.59.1):
resolution: {integrity: sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==} resolution: {integrity: sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -6668,8 +6671,8 @@ packages:
import-fresh: 3.3.0 import-fresh: 3.3.0
picocolors: 1.0.0 picocolors: 1.0.0
sade: 1.8.1 sade: 1.8.1
svelte: 3.58.0 svelte: 3.59.1
svelte-preprocess: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3) svelte-preprocess: 5.0.3(sass@1.61.0)(svelte@3.59.1)(typescript@5.0.3)
typescript: 5.0.3 typescript: 5.0.3
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@@ -6683,18 +6686,18 @@ packages:
- sugarss - sugarss
dev: true dev: true
/svelte-dnd-action@0.9.22(svelte@3.58.0): /svelte-dnd-action@0.9.22(svelte@3.59.1):
resolution: {integrity: sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==} resolution: {integrity: sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==}
peerDependencies: peerDependencies:
svelte: '>=3.23.0' svelte: '>=3.23.0'
dependencies: dependencies:
svelte: 3.58.0 svelte: 3.59.1
dev: true dev: true
/svelte-feather-icons@4.0.0: /svelte-feather-icons@4.0.0:
resolution: {integrity: sha512-4ieUsjp+VYa1r6y80jDt9zRiRUZyJNbESpRdHdJJhiBubyuXX96A7f1UZSK4olxzP6Qsg5ZAuyZlnmvD+/swAA==} resolution: {integrity: sha512-4ieUsjp+VYa1r6y80jDt9zRiRUZyJNbESpRdHdJJhiBubyuXX96A7f1UZSK4olxzP6Qsg5ZAuyZlnmvD+/swAA==}
dependencies: dependencies:
svelte: 3.58.0 svelte: 3.59.1
dev: false dev: false
/svelte-floating-ui@1.2.8: /svelte-floating-ui@1.2.8:
@@ -6718,6 +6721,16 @@ packages:
svelte: '>=3.19.0' svelte: '>=3.19.0'
dependencies: dependencies:
svelte: 3.58.0 svelte: 3.58.0
dev: true
/svelte-hmr@0.15.1(svelte@3.59.1):
resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
engines: {node: ^12.20 || ^14.13.1 || >= 16}
peerDependencies:
svelte: '>=3.19.0'
dependencies:
svelte: 3.59.1
dev: false
/svelte-preprocess@4.10.1(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4): /svelte-preprocess@4.10.1(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4):
resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==} resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==}
@@ -6825,7 +6838,7 @@ packages:
typescript: 5.0.3 typescript: 5.0.3
dev: true dev: true
/svelte-preprocess@5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3): /svelte-preprocess@5.0.3(sass@1.61.0)(svelte@3.59.1)(typescript@5.0.3):
resolution: {integrity: sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==} resolution: {integrity: sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==}
engines: {node: '>= 14.10.0'} engines: {node: '>= 14.10.0'}
requiresBuild: true requiresBuild: true
@@ -6869,7 +6882,7 @@ packages:
sass: 1.61.0 sass: 1.61.0
sorcery: 0.11.0 sorcery: 0.11.0
strip-indent: 3.0.0 strip-indent: 3.0.0
svelte: 3.58.0 svelte: 3.59.1
typescript: 5.0.3 typescript: 5.0.3
/svelte-range-slider-pips@2.0.1: /svelte-range-slider-pips@2.0.1:
@@ -6883,13 +6896,13 @@ packages:
svelte-floating-ui: 1.2.8 svelte-floating-ui: 1.2.8
dev: false dev: false
/svelte-splitpanes@0.7.13(svelte@3.58.0): /svelte-splitpanes@0.7.13(svelte@3.59.1):
resolution: {integrity: sha512-LiAf4OEZqRJanoax9mextXtQ0JzrdCqX2tOgVO+yJu2XNyGz5j5fGbw8+5AXgOasPi/m1nv8n2Lt+XYFRfvIGg==} resolution: {integrity: sha512-LiAf4OEZqRJanoax9mextXtQ0JzrdCqX2tOgVO+yJu2XNyGz5j5fGbw8+5AXgOasPi/m1nv8n2Lt+XYFRfvIGg==}
peerDependencies: peerDependencies:
svelte: ^3.54.0 svelte: ^3.54.0
dependencies: dependencies:
esm-env-robust: 0.0.3 esm-env-robust: 0.0.3
svelte: 3.58.0 svelte: 3.59.1
dev: false dev: false
/svelte-tiny-virtual-list@2.0.5: /svelte-tiny-virtual-list@2.0.5:
@@ -6912,6 +6925,10 @@ packages:
resolution: {integrity: sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==} resolution: {integrity: sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
/svelte@3.59.1:
resolution: {integrity: sha512-pKj8fEBmqf6mq3/NfrB9SLtcJcUvjYSWyePlfCqN9gujLB25RitWK8PvFzlwim6hD/We35KbPlRteuA6rnPGcQ==}
engines: {node: '>= 8'}
/swiper@9.2.4: /swiper@9.2.4:
resolution: {integrity: sha512-L7y3K/iiMXNYQ94FbfcJn7jex4QPnS4+voXGupTdC+UHW4XrR40QDdm4c9hXJ+Br0Il7PP0vP1W3goM9/Ly6Sg==} resolution: {integrity: sha512-L7y3K/iiMXNYQ94FbfcJn7jex4QPnS4+voXGupTdC+UHW4XrR40QDdm4c9hXJ+Br0Il7PP0vP1W3goM9/Ly6Sg==}
engines: {node: '>= 4.7.0'} engines: {node: '>= 4.7.0'}

View File

@@ -4,12 +4,12 @@
import "@litegraph-ts/core/css/litegraph.css"; import "@litegraph-ts/core/css/litegraph.css";
import "./scss/global.scss"; import "./scss/global.scss";
import { onMount } from 'svelte';
export let app: ComfyAppState; export let app: ComfyAppState;
export let isMobile: boolean
</script> </script>
<ComfyApp {app}/> {#if isMobile}
<div>Redirecting...</div>
<style> {:else}
</style> <ComfyApp {app}/>
{/if}

View File

@@ -2,22 +2,22 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { App, View } from "framework7-svelte" import { App, View, Preloader } from "framework7-svelte"
import { f7, f7ready } from 'framework7-svelte'; import { f7, f7ready } from 'framework7-svelte';
import "framework7/css/bundle" import "framework7/css/bundle"
import "./scss/global.scss"; import "./scss/global.scss";
import MainToolbar from './mobile/MainToolbar.svelte'
import GenToolbar from './mobile/GenToolbar.svelte' import GenToolbar from './mobile/GenToolbar.svelte'
import HomePage from './mobile/routes/home.svelte'; import WorkflowsPage from './mobile/routes/workflows.svelte';
import AboutPage from './mobile/routes/about.svelte'; import QueuePage from './mobile/routes/queue.svelte';
import LoginPage from './mobile/routes/login.svelte'; import GalleryPage from './mobile/routes/gallery.svelte';
import GraphPage from './mobile/routes/graph.svelte'; import WorkflowPage from './mobile/routes/workflow.svelte';
import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte';
import SubWorkflowPage from './mobile/routes/subworkflow.svelte';
import type { Framework7Parameters, Modal } from "framework7/types"; import type { Framework7Parameters, Modal } from "framework7/types";
import interfaceState from "$lib/stores/interfaceState";
export let app: ComfyApp; export let app: ComfyApp;
@@ -51,11 +51,40 @@
} }
} }
let appSetupPromise: Promise<void> = null;
let loading = true;
let lastSize = Number.POSITIVE_INFINITY;
$: f7 && f7.setDarkMode($interfaceState.isDarkMode)
onMount(async () => { onMount(async () => {
await app.setup(); // let isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
$interfaceState.isDarkMode = true;
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
$interfaceState.isDarkMode = event.matches;
});
appSetupPromise = app.setup().then(() => {
// Autosave every minute
setInterval(() => app.saveStateToLocalStorage(false), 60 * 1000)
loading = false
});
window.addEventListener("backbutton", onBackKeyDown, false); window.addEventListener("backbutton", onBackKeyDown, false);
window.addEventListener("popstate", onBackKeyDown, false); window.addEventListener("popstate", onBackKeyDown, false);
});
// Blur any input elements when the virtual keyboard closes
// Otherwise tapping on other input events can refocus the input from way
// off the screen
window.visualViewport.addEventListener("resize", function(e) {
if (e.target.height > lastSize) {
// Assume keyboard was hidden
(document.activeElement as HTMLElement)?.blur();
}
lastSize = e.target.height
})
})
/* /*
Now we need to map components to routes. Now we need to map components to routes.
@@ -66,36 +95,42 @@
routes: [ routes: [
{ {
path: '/', path: '/',
component: HomePage, component: WorkflowsPage,
options: { options: {
props: { app } props: { app }
} }
}, },
{ {
path: '/about/', path: '/workflows',
component: AboutPage, component: WorkflowsPage,
},
{
path: '/login/',
component: LoginPage,
},
{
path: '/graph/',
component: GraphPage,
options: { options: {
props: { app } props: { app }
} }
}, },
{ {
path: '/subworkflows/', path: '/queue/',
component: ListSubWorkflowsPage, component: QueuePage,
options: { options: {
props: { app } props: { app }
} }
}, },
{ {
path: '/subworkflows/:subworkflowID/', path: '/gallery/',
component: SubWorkflowPage, component: GalleryPage,
options: {
props: { app }
}
},
// {
// path: '/graph/',
// component: GraphPage,
// options: {
// props: { app }
// }
// },
{
path: '/workflows/:workflowIndex/',
component: WorkflowPage,
options: { options: {
props: { app } props: { app }
} }
@@ -113,23 +148,90 @@
actions: { actions: {
closeOnEscape: true, closeOnEscape: true,
}, },
touch: {
tapHold: true
} }
}
let body;
const bindBody = (node) => (body = node);
function setDarkClass(isDark: boolean) {
if (!body)
return;
if (isDark) {
body.classList.add("dark");
} else {
body.classList.remove("dark");
}
};
$: setDarkClass($interfaceState.isDarkMode);
</script> </script>
{#if app} <svelte:body use:bindBody />
<App theme="auto" name="ComfyBox" {...f7params}>
<App theme="auto" name="ComfyBox" {...f7params}>
{#if appSetupPromise}
{#await appSetupPromise}
<div class="comfy-app-loading">
<div>
<Preloader color="blue" size={100} />
</div>
</div>
{:then}
<View <View
url="/" url="/workflows/"
main={true} main={true}
class="safe-areas" class="safe-areas"
masterDetailBreakpoint={768}, masterDetailBreakpoint={768},
browserHistory=true, browserHistory=true,
browserHistoryRoot="/mobile/" browserHistoryRoot="/mobile/"
> >
<MainToolbar {app} />
{#if $interfaceState.selectedWorkflowIndex && $interfaceState.showingWorkflow}
<GenToolbar {app} /> <GenToolbar {app} />
{/if}
</View> </View>
</App> {:catch error}
<div class="canvas-wrapper pane-wrapper" style="display: none"> <div class="comfy-loading-error">
<canvas id="graph-canvas" /> <div>
Error loading app
</div> </div>
{/if} <div>{error}</div>
{#if error != null && error.stack}
{@const lines = error.stack.split("\n")}
{#each lines as line}
<div style:font-size="16px">{line}</div>
{/each}
{/if}
</div>
{/await}
{/if}
</App>
<div class="canvas-wrapper pane-wrapper" style="display: none">
<canvas id="graph-canvas" />
</div>
<style lang="scss">
.comfy-app-loading, .comfy-loading-error {
font-size: 40px;
color: var(--body-text-color);
justify-content: center;
margin: auto;
width: 100%;
height: 100%;
text-align: center;
flex-direction: column;
display: flex;
position: absolute;
z-index: 100000000;
pointer-events: none;
user-select: none;
top: 0px;
}
.comfy-app-loading > span {
display: flex;
flex-direction: row;
justify-content: center;
}
</style>

View File

@@ -334,6 +334,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
* Handle keypress * Handle keypress
* *
* Ctrl + M mute/unmute selected nodes * Ctrl + M mute/unmute selected nodes
* Ctrl + Space open node searchbox
*/ */
override processKey(e: KeyboardEvent): boolean | undefined { override processKey(e: KeyboardEvent): boolean | undefined {
const res = super.processKey(e); const res = super.processKey(e);
@@ -353,7 +354,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
} }
if (e.type == "keydown") { if (e.type == "keydown") {
// Ctrl + M mute/unmute // Ctrl + M - mute/unmute
if (e.keyCode == 77 && e.ctrlKey) { if (e.keyCode == 77 && e.ctrlKey) {
if (this.selected_nodes) { if (this.selected_nodes) {
for (var i in this.selected_nodes) { for (var i in this.selected_nodes) {
@@ -366,6 +367,21 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
} }
block_default = true; block_default = true;
} }
// Ctrl + Space - open node searchbox
else if (e.keyCode == 32 && e.ctrlKey) {
const event = new MouseEvent("click");
const searchBox = this.showSearchBox(event);
const rect = this.canvas.getBoundingClientRect();
const sbRect = searchBox.getBoundingClientRect();
const clientX = rect.left + rect.width / 2 - sbRect.width / 2;
const clientY = rect.top + rect.height / 2 - sbRect.height / 2
searchBox.style.left = `${clientX}px`;
searchBox.style.top = `${clientY}px`;
// TODO better API
event.initMouseEvent("click", true, true, window, 1, clientX, clientY, clientX, clientY, false, false, false, false, 0, null);
this.adjustMouseEvent(event);
block_default = true;
}
} }
this.graph.change(); this.graph.change();

View File

@@ -125,8 +125,17 @@ export default class ComfyAPI {
}, 1000); }, 1000);
} }
private getHostname(): string {
let hostname = this.hostname || location.hostname;
if (hostname === "localhost") {
// For dev use, assume same hostname as connected server
hostname = location.hostname;
}
return hostname;
}
private getBackendUrl(): string { private getBackendUrl(): string {
const hostname = this.hostname || location.hostname; const hostname = this.getHostname()
const port = this.port || location.port; const port = this.port || location.port;
return `${window.location.protocol}//${hostname}:${port}` return `${window.location.protocol}//${hostname}:${port}`
} }
@@ -146,7 +155,7 @@ export default class ComfyAPI {
existingSession = "?clientId=" + existingSession; existingSession = "?clientId=" + existingSession;
} }
const hostname = this.hostname || location.hostname; const hostname = this.getHostname()
const port = this.port || location.port; const port = this.port || location.port;
this.socket = new WebSocket( this.socket = new WebSocket(

View File

@@ -211,7 +211,13 @@ export default class ComfyApp {
this.lCanvas.allow_interaction = uiUnlocked; this.lCanvas.allow_interaction = uiUnlocked;
// await this.#invokeExtensionsAsync("init"); // await this.#invokeExtensionsAsync("init");
const defs = await this.api.getNodeDefs(); let defs;
try {
defs = await this.api.getNodeDefs();
}
catch (error) {
throw new Error(`Could not reach ComfyUI at ${this.api.getBackendUrl()}`);
}
await this.registerNodes(defs); await this.registerNodes(defs);
// Load previous workflow // Load previous workflow
@@ -362,7 +368,7 @@ export default class ComfyApp {
} }
} }
saveStateToLocalStorage() { saveStateToLocalStorage(doNotify: boolean = true) {
try { try {
uiState.update(s => { s.forceSaveUserState = true; return s; }) uiState.update(s => { s.forceSaveUserState = true; return s; })
const state = get(workflowState) const state = get(workflowState)
@@ -374,9 +380,11 @@ export default class ComfyApp {
for (const workflow of workflows) for (const workflow of workflows)
workflow.isModified = false; workflow.isModified = false;
workflowState.set(get(workflowState)); workflowState.set(get(workflowState));
if (doNotify)
notify("Saved to local storage.") notify("Saved to local storage.")
} }
catch (err) { catch (err) {
if (doNotify)
notify(`Failed saving to local storage:\n${err}`, { type: "error" }) notify(`Failed saving to local storage:\n${err}`, { type: "error" })
} }
finally { finally {
@@ -395,6 +403,9 @@ export default class ComfyApp {
return false; return false;
const workflows = state.workflows as SerializedAppState[]; const workflows = state.workflows as SerializedAppState[];
if (workflows.length === 0)
return false;
await Promise.all(workflows.map(w => { await Promise.all(workflows.map(w => {
return this.openWorkflow(w, { refreshCombos: defs, warnMissingNodeTypes: false, setActive: false }).catch(error => { return this.openWorkflow(w, { refreshCombos: defs, warnMissingNodeTypes: false, setActive: false }).catch(error => {
console.error("Failed restoring previous workflow", error) console.error("Failed restoring previous workflow", error)

View File

@@ -30,6 +30,7 @@
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte"; import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte"; import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte"; import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
import uiQueueState from "$lib/stores/uiQueueState";
export let app: ComfyApp; export let app: ComfyApp;
@@ -52,46 +53,34 @@
let displayMode: DisplayModeType = "list"; let displayMode: DisplayModeType = "list";
let imageSize: number = 40; let imageSize: number = 40;
let gridColumns: number = 3; let gridColumns: number = 3;
let changed = true;
function switchMode(newMode: QueueItemType) { function switchMode(newMode: QueueItemType) {
changed = mode !== newMode const changed = mode !== newMode
mode = newMode mode = newMode
if (changed) { if (changed) {
_queuedEntries = [] uiQueueState.updateEntries();
_runningEntries = []
_entries = []
} }
} }
function switchDisplayMode(newDisplayMode: DisplayModeType) { function switchDisplayMode(newDisplayMode: DisplayModeType) {
// changed = displayMode !== newDisplayMode
displayMode = newDisplayMode displayMode = newDisplayMode
// if (changed) {
// _queuedEntries = []
// _runningEntries = []
// _entries = []
// }
} }
let _queuedEntries: QueueUIEntry[] = [] let _entries: ReadonlyArray<QueueUIEntry> = []
let _runningEntries: QueueUIEntry[] = [] $: if(mode === "queue") {
let _entries: QueueUIEntry[] = [] _entries = $uiQueueState.queueUIEntries
$: if (mode === "queue" && (changed || $queuePending.length != _queuedEntries.length || $queueRunning.length != _runningEntries.length)) {
updateFromQueue(); updateFromQueue();
changed = false;
} }
else if (mode === "history" && (changed || $queueCompleted.length != _entries.length)) { else {
_entries = $uiQueueState.historyUIEntries;
updateFromHistory(); updateFromHistory();
changed = false;
} }
$: if (mode === "queue" && !$queuePending && !$queueRunning) { $: if (mode === "queue" && !$queuePending && !$queueRunning) {
_queuedEntries = [] uiQueueState.clearQueue();
_runningEntries = [] }
_entries = []; else if (mode === "history" && !$queueCompleted) {
changed = true uiQueueState.clearHistory();
} }
async function deleteEntry(entry: QueueUIEntry, event: MouseEvent) { async function deleteEntry(entry: QueueUIEntry, event: MouseEvent) {
@@ -106,125 +95,26 @@
await app.deleteQueueItem(mode, entry.entry.promptID); await app.deleteQueueItem(mode, entry.entry.promptID);
} }
if (mode === "queue") { uiQueueState.updateEntries(true)
_queuedEntries = []
_runningEntries = []
}
_entries = [];
changed = true;
} }
async function clearQueue() { async function clearQueue() {
await app.clearQueue(mode); await app.clearQueue(mode);
uiQueueState.updateEntries(true)
if (mode === "queue") {
_queuedEntries = []
_runningEntries = []
}
_entries = [];
changed = true;
}
function formatDate(date: Date): string {
const time = date.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true });
const day = date.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }).replace(',', '');
return [time, day].join(", ")
}
function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
let date = entry.finishedAt || entry.queuedAt;
let dateStr = null;
if (date) {
dateStr = formatDate(date);
}
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs;
let message = "Prompt";
if (entry.extraData?.workflowTitle != null) {
message = `${entry.extraData.workflowTitle}`
}
if (subgraphs && subgraphs.length > 0) {
const subgraphsString = subgraphs.join(', ')
message += ` (${subgraphsString})`
}
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
if (Object.keys(entry.outputs).length > 0) {
const imageCount = Object.values(entry.outputs).filter(o => o.images).flatMap(o => o.images).length
submessage = `Images: ${imageCount}`
}
return {
entry,
message,
submessage,
date: dateStr,
status,
images: []
}
}
function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
const result = convertEntry(entry, status);
const thumbnails = entry.extraData?.thumbnails
if (thumbnails) {
result.images = thumbnails.map(convertComfyOutputToComfyURL);
}
const outputs = Object.values(entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
if (outputs) {
result.images = result.images.concat(outputs)
}
return result;
}
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
const result = convertEntry(entry.entry, entry.status);
const images = Object.values(entry.entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
result.images = images
if (entry.message)
result.submessage = entry.message
else if (entry.status === "interrupted" || entry.status === "all_cached")
result.submessage = "Prompt was interrupted."
if (entry.error)
result.error = entry.error
return result;
} }
async function updateFromQueue() { async function updateFromQueue() {
// newest entries appear at the top
_queuedEntries = $queuePending.map((e) => convertPendingEntry(e, "pending")).reverse();
_runningEntries = $queueRunning.map((e) => convertPendingEntry(e, "running")).reverse();
_entries = [..._queuedEntries, ..._runningEntries]
if (queueList) { if (queueList) {
await tick(); // Wait for list size to be recalculated await tick(); // Wait for list size to be recalculated
queueList.scroll({ top: queueList.scrollHeight }) queueList.scroll({ top: queueList.scrollHeight })
} }
console.warn("[ComfyQueue] BUILDQUEUE", _entries.length, $queuePending.length, $queueRunning.length)
} }
async function updateFromHistory() { async function updateFromHistory() {
_entries = $queueCompleted.map(convertCompletedEntry).reverse();
if (queueList) { if (queueList) {
await tick(); // Wait for list size to be recalculated
queueList.scrollTo(0, 0); queueList.scrollTo(0, 0);
} }
console.warn("[ComfyQueue] BUILDHISTORY", _entries.length, $queueCompleted.length)
} }
async function interrupt() { async function interrupt() {

View File

@@ -0,0 +1,58 @@
<!--
Fix a framework7 issue
https://github.com/framework7io/framework7/issues/4183
-->
<script>
let className = undefined;
export { className as class };
export let progress = 0;
export let infinite = false;
function colorClasses(props) {
const { color, textColor, bgColor, borderColor, rippleColor, dark } = props;
return {
dark,
[`color-${color}`]: color,
[`text-color-${textColor}`]: textColor,
[`bg-color-${bgColor}`]: bgColor,
[`border-color-${borderColor}`]: borderColor,
[`ripple-color-${rippleColor}`]: rippleColor,
};
}
function classNames(...args) {
const classes = [];
args.forEach((arg) => {
if (typeof arg === 'object' && arg.constructor === Object) {
Object.keys(arg).forEach((key) => {
if (arg[key]) classes.push(key);
});
} else if (arg) classes.push(arg);
});
const uniqueClasses = [];
classes.forEach((c) => {
if (uniqueClasses.indexOf(c) < 0) uniqueClasses.push(c);
});
return uniqueClasses.join(' ');
}
let classes
$: classes = classNames(
className,
'progressbar',
{
'progressbar-infinite': infinite,
},
colorClasses($$props),
);
let transformStyle = ""
$: transformStyle = progress ? `translate3d(${-100 + progress}%, 0, 0)` : '';
</script>
<span class={classes}
data-progress={progress} >
<span style:transform={transformStyle} />
</span>

View File

@@ -2,6 +2,9 @@ import type ComfyGraphCanvas from "$lib/ComfyGraphCanvas";
import { type ContainerLayout, type IDragItem, type TemplateLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates" import { type ContainerLayout, type IDragItem, type TemplateLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
import type { LGraphCanvas, Vector2 } from "@litegraph-ts/core"; import type { LGraphCanvas, Vector2 } from "@litegraph-ts/core";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { PhotoBrowser, f7 } from "framework7-svelte";
import { ImageViewer } from "$lib/ImageViewer";
import interfaceState from "$lib/stores/interfaceState";
export function handleContainerConsider(layoutState: WritableLayoutStateStore, container: ContainerLayout, evt: CustomEvent<DndEvent<IDragItem>>): IDragItem[] { export function handleContainerConsider(layoutState: WritableLayoutStateStore, container: ContainerLayout, evt: CustomEvent<DndEvent<IDragItem>>): IDragItem[] {
return layoutState.updateChildren(container, evt.detail.items) return layoutState.updateChildren(container, evt.detail.items)
@@ -45,3 +48,25 @@ function doInsertTemplate(layoutState: WritableLayoutStateStore, droppedTemplate
return get(layoutState).allItems[container.id].children; return get(layoutState).allItems[container.id].children;
} }
let mobileLightbox = null;
export function showMobileLightbox(images: any[], selectedImage: number, options: Partial<PhotoBrowser["params"]> = {}) {
if (!f7)
return
if (mobileLightbox) {
mobileLightbox.destroy();
mobileLightbox = null;
}
history.pushState({ type: "gallery" }, "");
mobileLightbox = f7.photoBrowser.create({
photos: images,
theme: get(interfaceState).isDarkMode ? "dark" : "light",
type: 'popup',
...options
});
mobileLightbox.open(selectedImage)
}

View File

@@ -8,6 +8,7 @@ import notify from "$lib/notify";
import workflowState from "$lib/stores/workflowState"; import workflowState from "$lib/stores/workflowState";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type ComfyApp from "$lib/components/ComfyApp"; import type ComfyApp from "$lib/components/ComfyApp";
import interfaceState from "$lib/stores/interfaceState";
export interface ComfySendOutputActionProperties extends ComfyGraphNodeProperties { export interface ComfySendOutputActionProperties extends ComfyGraphNodeProperties {
} }
@@ -41,36 +42,8 @@ export default class ComfySendOutputAction extends ComfyGraphNode {
this.isActive = true; this.isActive = true;
const doSend = (modal: ModalData) => { interfaceState.querySendOutput(value, type, receiveTargets, () => {
this.isActive = false; this.isActive = false;
const { workflow, targetNode } = get(modal.state) as SendOutputModalResult;
console.warn("send", workflow, targetNode);
if (workflow == null || targetNode == null)
return
const app = (window as any).app as ComfyApp;
if (app == null) {
console.error("Couldn't get app!")
return
}
targetNode.receiveOutput(value);
workflowState.setActiveWorkflow(app.lCanvas, workflow.id)
}
modalState.pushModal({
title: "Send Output",
closeOnClick: true,
showCloseButton: true,
svelteComponent: SendOutputModal,
svelteProps: {
value,
type,
receiveTargets
},
onClose: doSend
}) })
}; };
} }

View File

@@ -42,6 +42,7 @@ export default class ComfyMarkdownNode extends ComfyWidgetNode<string> {
}, },
{ {
multiline: true, multiline: true,
inputStyle: { fontFamily: "monospace" } inputStyle: { fontFamily: "monospace" }
} }
) )

View File

@@ -27,6 +27,11 @@ function notifyf7(text: string, options: NotifyOptions) {
on.click = () => options.onClick(); on.click = () => options.onClick();
} }
let icon = null;
if (options.imageUrl) {
icon = `<img src="${options.imageUrl}"/>`
}
const notification = f7.notification.create({ const notification = f7.notification.create({
title: options.title, title: options.title,
titleRightText: 'now', titleRightText: 'now',
@@ -34,7 +39,8 @@ function notifyf7(text: string, options: NotifyOptions) {
text: text, text: text,
closeOnClick: true, closeOnClick: true,
closeTimeout, closeTimeout,
on on,
icon
}); });
notification.open(); notification.open();
} }

View File

@@ -24,7 +24,12 @@ let changedOptions: Partial<Record<keyof ConfigState, [any, any]>> = {}
function getBackendURL(): string { function getBackendURL(): string {
const state = get(store); const state = get(store);
return `${window.location.protocol}//${state.comfyUIHostname}:${state.comfyUIPort}` let hostname = state.comfyUIHostname
if (hostname === "localhost") {
// For dev use, assume same hostname as connected server
hostname = location.hostname;
}
return `${window.location.protocol}//${hostname}:${state.comfyUIPort}`
} }
function canShowNotificationText(): boolean { function canShowNotificationText(): boolean {

View File

@@ -1,6 +1,12 @@
import { debounce } from '$lib/utils'; import { debounce, isMobileBrowser } from '$lib/utils';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import type { WorkflowInstID, WorkflowReceiveOutputTargets } from './workflowState';
import modalState, { type ModalData } from './modalState';
import type { SlotType } from '@litegraph-ts/core';
import type ComfyApp from '$lib/components/ComfyApp';
import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte";
import workflowState from './workflowState';
export type InterfaceState = { export type InterfaceState = {
// Show a large indicator of the currently editing number value for mobile // Show a large indicator of the currently editing number value for mobile
@@ -10,12 +16,20 @@ export type InterfaceState = {
showIndicator: boolean, showIndicator: boolean,
indicatorValue: any, indicatorValue: any,
graphTransitioning: boolean graphTransitioning: boolean,
isJumpingToNode: boolean isJumpingToNode: boolean,
selectedWorkflowIndex: number | null
showingWorkflow: boolean,
selectedTab: number,
showSheet: boolean,
isDarkMode: boolean
} }
type InterfaceStateOps = { type InterfaceStateOps = {
showIndicator: (pointerX: number, pointerY: number, value: any) => void, showIndicator: (pointerX: number, pointerY: number, value: any) => void,
querySendOutput: (value: any, type: SlotType, receiveTargets: WorkflowReceiveOutputTargets[], cb: (modal: ModalData) => void) => void,
} }
export type WritableInterfaceStateStore = Writable<InterfaceState> & InterfaceStateOps; export type WritableInterfaceStateStore = Writable<InterfaceState> & InterfaceStateOps;
@@ -28,6 +42,13 @@ const store: Writable<InterfaceState> = writable(
graphTransitioning: false, graphTransitioning: false,
isJumpingToNode: false, isJumpingToNode: false,
selectedTab: 1,
showSheet: false,
selectedWorkflowIndex: null,
showingWorkflow: false,
isDarkMode: false,
}) })
const debounceDrag = debounce(() => { store.update(s => { s.showIndicator = false; return s }) }, 1000) const debounceDrag = debounce(() => { store.update(s => { s.showIndicator = false; return s }) }, 1000)
@@ -46,9 +67,49 @@ function showIndicator(pointerX: number, pointerY: number, value: any) {
debounceDrag(); debounceDrag();
} }
function querySendOutput(value: any, type: SlotType, receiveTargets: WorkflowReceiveOutputTargets[], cb: (modal: ModalData) => void) {
if (isMobileBrowser(navigator.userAgent)) {
store.update(s => { s.showSheet = true; return s; })
}
else {
const doSend = (modal: ModalData) => {
cb(modal)
const { workflow, targetNode } = get(modal.state) as SendOutputModalResult;
console.warn("send", workflow, targetNode);
if (workflow == null || targetNode == null)
return
const app = (window as any).app as ComfyApp;
if (app == null) {
console.error("Couldn't get app!")
return
}
targetNode.receiveOutput(value);
workflowState.setActiveWorkflow(app.lCanvas, workflow.id)
}
modalState.pushModal({
title: "Send Output",
closeOnClick: true,
showCloseButton: true,
svelteComponent: SendOutputModal,
svelteProps: {
value,
type,
receiveTargets
},
onClose: doSend
})
}
}
const interfaceStateStore: WritableInterfaceStateStore = const interfaceStateStore: WritableInterfaceStateStore =
{ {
...store, ...store,
showIndicator showIndicator,
querySendOutput
} }
export default interfaceStateStore; export default interfaceStateStore;

View File

@@ -8,6 +8,7 @@ import { get, writable, type Writable } from "svelte/store";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import workflowState, { type WorkflowError, type WorkflowExecutionError, type WorkflowInstID, type WorkflowValidationError } from "./workflowState"; import workflowState, { type WorkflowError, type WorkflowExecutionError, type WorkflowInstID, type WorkflowValidationError } from "./workflowState";
import configState from "./configState"; import configState from "./configState";
import uiQueueState from "./uiQueueState";
export type QueueEntryStatus = "success" | "validation_failed" | "error" | "interrupted" | "all_cached" | "unknown"; export type QueueEntryStatus = "success" | "validation_failed" | "error" | "interrupted" | "all_cached" | "unknown";
@@ -444,7 +445,6 @@ function queueCleared(type: QueueItemType) {
store.update(s => { store.update(s => {
if (type === "queue") { if (type === "queue") {
s.queuePending.set([]); s.queuePending.set([]);
s.queueRunning.set([]);
s.queueRemaining = 0; s.queueRemaining = 0;
s.runningNodeID = null; s.runningNodeID = null;
s.progress = null; s.progress = null;

View File

@@ -0,0 +1,207 @@
import type { PromptID, QueueItemType } from '$lib/api';
import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import queueState, { type CompletedQueueEntry, type QueueEntry } from './queueState';
import type { WorkflowError } from './workflowState';
import { convertComfyOutputToComfyURL } from '$lib/utils';
export type QueueUIEntryStatus = QueueEntryStatus | "pending" | "running";
export type QueueUIEntry = {
entry: QueueEntry,
message: string,
submessage: string,
date?: string,
status: QueueUIEntryStatus,
images?: string[], // URLs
details?: string, // shown in a tooltip on hover
error?: WorkflowError
}
export type UIQueueState = {
mode: QueueItemType,
queuedEntries: QueueUIEntry[],
runningEntries: QueueUIEntry[],
queueUIEntries: QueueUIEntry[],
historyUIEntries: QueueUIEntry[],
}
type UIQueueStateOps = {
updateEntries: (force?: boolean) => void
clearAll: () => void
clearQueue: () => void
clearHistory: () => void
}
export type WritableUIQueueStateStore = Writable<UIQueueState> & UIQueueStateOps;
const store: Writable<UIQueueState> = writable(
{
mode: "queue",
queuedEntries: [],
runningEntries: [],
completedEntries: [],
queueUIEntries: [],
historyUIEntries: [],
})
function formatDate(date: Date): string {
const time = date.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true });
const day = date.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }).replace(',', '');
return [time, day].join(", ")
}
function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
let date = entry.finishedAt || entry.queuedAt;
let dateStr = null;
if (date) {
dateStr = formatDate(date);
}
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs;
let message = "Prompt";
if (entry.extraData?.workflowTitle != null) {
message = `${entry.extraData.workflowTitle}`
}
if (subgraphs && subgraphs.length > 0) {
const subgraphsString = subgraphs.join(', ')
message += ` (${subgraphsString})`
}
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
if (Object.keys(entry.outputs).length > 0) {
const imageCount = Object.values(entry.outputs).filter(o => o.images).flatMap(o => o.images).length
submessage = `Images: ${imageCount}`
}
return {
entry,
message,
submessage,
date: dateStr,
status,
images: []
}
}
function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
const result = convertEntry(entry, status);
const thumbnails = entry.extraData?.thumbnails
if (thumbnails) {
result.images = thumbnails.map(convertComfyOutputToComfyURL);
}
const outputs = Object.values(entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
if (outputs) {
result.images = result.images.concat(outputs)
}
return result;
}
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
const result = convertEntry(entry.entry, entry.status);
const images = Object.values(entry.entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
result.images = images
if (entry.message)
result.submessage = entry.message
else if (entry.status === "interrupted" || entry.status === "all_cached")
result.submessage = "Prompt was interrupted."
if (entry.error)
result.error = entry.error
return result;
}
function updateFromQueue(queuePending: QueueEntry[], queueRunning: QueueEntry[]) {
store.update(s => {
// newest entries appear at the top
s.queuedEntries = queuePending.map((e) => convertPendingEntry(e, "pending")).reverse();
s.runningEntries = queueRunning.map((e) => convertPendingEntry(e, "running")).reverse();
s.queueUIEntries = s.queuedEntries.concat(s.runningEntries);
console.warn("[ComfyQueue] BUILDQUEUE", s.queuedEntries.length, s.runningEntries.length)
return s;
})
}
function updateFromHistory(queueCompleted: CompletedQueueEntry[]) {
store.update(s => {
s.historyUIEntries = queueCompleted.map(convertCompletedEntry).reverse();
console.warn("[ComfyQueue] BUILDHISTORY", s.historyUIEntries.length)
return s
})
}
function updateEntries(force: boolean = false) {
const state = get(store)
const qs = get(queueState)
const queuePending = qs.queuePending
const queueRunning = qs.queueRunning
const queueCompleted = qs.queueCompleted
const queueChanged = get(queuePending).length != state.queuedEntries.length
|| get(queueRunning).length != state.runningEntries.length;
const historyChanged = get(queueCompleted).length != state.historyUIEntries.length;
if (queueChanged || force) {
updateFromQueue(get(queuePending), get(queueRunning));
}
if (historyChanged || force) {
updateFromHistory(get(queueCompleted));
}
}
function clearAll() {
store.update(s => {
s.queuedEntries = []
s.runningEntries = []
s.historyUIEntries = []
return s
})
updateEntries(true);
}
function clearQueue() {
store.update(s => {
s.queuedEntries = []
s.runningEntries = []
return s
})
updateEntries(true);
}
function clearHistory() {
store.update(s => {
s.historyUIEntries = []
return s
})
updateEntries(true);
}
queueState.subscribe(s => {
updateEntries();
})
const uiStateStore: WritableUIQueueStateStore =
{
...store,
updateEntries,
clearAll,
clearQueue,
clearHistory,
}
export default uiStateStore;

View File

@@ -4,10 +4,11 @@ import type { FileData as GradioFileData } from "@gradio/upload";
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID, type SlotType, type Vector4, type SerializedLGraphNode } from "@litegraph-ts/core"; import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID, type SlotType, type Vector4, type SerializedLGraphNode } from "@litegraph-ts/core";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { ComfyNodeID } from "./api"; import type { ComfyNodeID } from "./api";
import { type SerializedPrompt } from "./components/ComfyApp"; import ComfyApp, { type SerializedPrompt } from "./components/ComfyApp";
import workflowState from "./stores/workflowState"; import workflowState, { type WorkflowReceiveOutputTargets } from "./stores/workflowState";
import { ImageViewer } from "./ImageViewer"; import { ImageViewer } from "./ImageViewer";
import configState from "$lib/stores/configState"; import configState from "$lib/stores/configState";
import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte";
export function clamp(n: number, min: number, max: number): number { export function clamp(n: number, min: number, max: number): number {
if (max <= min) if (max <= min)
@@ -618,6 +619,9 @@ export async function readFileToText(file: File): Promise<string> {
reader.onload = async () => { reader.onload = async () => {
resolve(reader.result as string); resolve(reader.result as string);
}; };
reader.onerror = async () => {
reject(reader.error);
}
reader.readAsText(file); reader.readAsText(file);
}) })
} }
@@ -709,3 +713,31 @@ export function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
canvas.toBlob(resolve); canvas.toBlob(resolve);
}); });
} }
export type SafetensorsMetadata = Record<string, string>
export async function getSafetensorsMetadata(folder: string, filename: string): Promise<SafetensorsMetadata> {
const url = configState.getBackendURL();
const params = new URLSearchParams({ filename })
return fetch(new Request(url + `/view_metadata/${folder}?` + params)).then(r => r.json())
}
export function partition<T>(myArray: T[], chunkSize: number): T[] {
let index = 0;
const arrayLength = myArray.length;
const tempArray = [];
for (index = 0; index < arrayLength; index += chunkSize) {
const myChunk = myArray.slice(index, index + chunkSize);
tempArray.push(myChunk);
}
return tempArray;
}
const MOBILE_USER_AGENTS = ["iPhone", "iPad", "Android", "BlackBerry", "WebOs"].map(a => new RegExp(a, "i"))
export function isMobileBrowser(userAgent: string): boolean {
return MOBILE_USER_AGENTS.some(a => userAgent.match(a))
}

View File

@@ -8,6 +8,7 @@
import { type WidgetLayout } from "$lib/stores/layoutStates"; import { type WidgetLayout } from "$lib/stores/layoutStates";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { isDisabled } from "./utils" import { isDisabled } from "./utils"
import { getSafetensorsMetadata } from '$lib/utils';
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
let node: ComfyComboNode | null = null; let node: ComfyComboNode | null = null;
@@ -73,26 +74,10 @@
} }
} }
function onSelect(e: CustomEvent<any>) {
if (input)
input.blur();
navigator.vibrate(20)
const item = e.detail
console.debug("[ComboWidget] SELECT", item, item.index)
$nodeValue = item.value;
activeIndex = item.index;
listOpen = false;
}
let activeIndex = null; let activeIndex = null;
let hoverItemIndex = null; let hoverItemIndex = null;
let filterText = ""; let filterText = "";
let listOpen = null; let listOpen = null;
let scrollToIndex = null;
let start = 0;
let end = 0;
function handleHover(index: number) { function handleHover(index: number) {
// console.warn("HOV", index) // console.warn("HOV", index)
@@ -107,7 +92,9 @@
$nodeValue = item.value $nodeValue = item.value
listOpen = false; listOpen = false;
filterText = "" filterText = ""
input?.blur() setTimeout(() => {
input?.blur();
}, 100)
} }
function onFilter() { function onFilter() {
@@ -173,7 +160,10 @@
on:select={(e) => handleSelect(e.detail.index)} on:select={(e) => handleSelect(e.detail.index)}
on:blur on:blur
on:filter={onFilter}> on:filter={onFilter}>
<div class="comfy-select-list" slot="list" let:filteredItems style:--maxLabelWidth={node.maxLabelWidthChars || 100}> <div class="comfy-select-list" slot="list"
class:mobile={isMobile}
let:filteredItems
style:--maxLabelWidth={node.maxLabelWidthChars || 100}>
{#if filteredItems.length > 0} {#if filteredItems.length > 0}
{@const itemSize = isMobile ? 50 : 25} {@const itemSize = isMobile ? 50 : 25}
{@const itemsToShow = isMobile ? 10 : 30} {@const itemsToShow = isMobile ? 10 : 30}
@@ -291,9 +281,14 @@
.comfy-select-list { .comfy-select-list {
--maxLabelWidth: 100; --maxLabelWidth: 100;
--maxListWidth: 50vw;
&.mobile {
--maxListWidth: 80vw;
}
font-size: 14px; font-size: 14px;
width: min(calc((var(--maxLabelWidth) + 10) * 1ch), 50vw);
color: var(--item-color); color: var(--item-color);
width: min(calc((var(--maxLabelWidth) + 10) * 1ch), var(--maxListWidth));
> :global(.virtual-list-wrapper) { > :global(.virtual-list-wrapper) {
box-shadow: var(--block-shadow); box-shadow: var(--block-shadow);

View File

@@ -11,6 +11,7 @@
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils"; import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
import { f7 } from "framework7-svelte"; import { f7 } from "framework7-svelte";
import type { ComfyGalleryNode } from "$lib/nodes/widgets"; import type { ComfyGalleryNode } from "$lib/nodes/widgets";
import { showMobileLightbox } from "$lib/components/utils";
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
@@ -47,19 +48,8 @@
object_fit: "cover", object_fit: "cover",
// preview: true // preview: true
} }
let element: HTMLDivElement;
let mobileLightbox = null;
function showMobileLightbox(source: HTMLImageElement) {
if (!f7)
return
if (mobileLightbox) {
mobileLightbox.destroy();
mobileLightbox = null;
}
function showMobileLightbox_(source: HTMLImageElement, selectedImage: number) {
const galleryElem = source.closest<HTMLDivElement>("div.block") const galleryElem = source.closest<HTMLDivElement>("div.block")
console.debug("[ImageViewer] showModal", source, galleryElem); console.debug("[ImageViewer] showModal", source, galleryElem);
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) { if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
@@ -72,23 +62,16 @@
const images = allGalleryButtons.map(button => { const images = allGalleryButtons.map(button => {
return { return {
url: (button.children[0] as HTMLImageElement).src, url: (button.children[0] as HTMLImageElement).src,
caption: "Image" // caption: "Image"
} }
}) })
history.pushState({ type: "gallery" }, ""); showMobileLightbox(images, selectedImage, { thumbs: images });
mobileLightbox = f7.photoBrowser.create({
photos: images,
thumbs: images.map(i => i.url),
type: 'popup',
});
mobileLightbox.open($selected_image)
} }
function onClicked(e: CustomEvent<HTMLImageElement>) { function onClicked(e: CustomEvent<HTMLImageElement>) {
if (isMobile) { if (isMobile) {
showMobileLightbox(e.detail) showMobileLightbox_(e.detail, $selected_image)
} }
else { else {
ImageViewer.instance.showLightbox(e.detail) ImageViewer.instance.showLightbox(e.detail)
@@ -103,7 +86,7 @@
{#if widget && node && nodeValue && $nodeValue} {#if widget && node && nodeValue && $nodeValue}
{#if widget.attrs.variant === "image"} {#if widget.attrs.variant === "image"}
<div class="wrapper comfy-image-widget" style={widget.attrs.style || ""} bind:this={element}> <div class="wrapper comfy-image-widget" style={widget.attrs.style || ""}>
<Block variant="solid" padding={false}> <Block variant="solid" padding={false}>
{#if $nodeValue && $nodeValue.length > 0} {#if $nodeValue && $nodeValue.length > 0}
{@const value = $nodeValue[$nodeValue.length-1]} {@const value = $nodeValue[$nodeValue.length-1]}
@@ -122,7 +105,7 @@
</div> </div>
{:else} {:else}
{@const images = $nodeValue.map(comfyBoxImageToComfyURL)} {@const images = $nodeValue.map(comfyBoxImageToComfyURL)}
<div class="wrapper comfy-gallery-widget gradio-gallery" style={widget.attrs.style || ""} bind:this={element}> <div class="wrapper comfy-gallery-widget gradio-gallery" style={widget.attrs.style || ""}>
<Block variant="solid" padding={false}> <Block variant="solid" padding={false}>
<div class="padding"> <div class="padding">
<Gallery <Gallery

View File

@@ -1,3 +1,15 @@
import { isMobileBrowser } from "$lib/utils"
const isMobile = isMobileBrowser(navigator.userAgent);
const params = new URLSearchParams(window.location.search)
if (params.get("desktop") !== "true") {
if (isMobile) {
window.location.href = "/mobile/"
}
}
// Run node registration before anthing else, in the proper order // Run node registration before anthing else, in the proper order
import "$lib/nodeImports"; import "$lib/nodeImports";
@@ -12,7 +24,7 @@ const comfyApp = new ComfyApp();
const app = new App({ const app = new App({
target: document.getElementById("app-root"), target: document.getElementById("app-root"),
props: { app: comfyApp } props: { app: comfyApp, isMobile }
}) })
export default app; export default app;

View File

@@ -1,19 +1,11 @@
<script lang="ts"> <script lang="ts">
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import queueState from "$lib/stores/queueState";
import workflowState, { ComfyBoxWorkflow } from "$lib/stores/workflowState"; import workflowState, { ComfyBoxWorkflow } from "$lib/stores/workflowState";
import { getNodeInfo } from "$lib/utils"
import { Link, Toolbar } from "framework7-svelte" import { Link, Toolbar } from "framework7-svelte"
import ProgressBar from "$lib/components/ProgressBar.svelte";
import Indicator from "./Indicator.svelte";
import interfaceState from "$lib/stores/interfaceState";
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
export let subworkflowID: number = -1; export let subworkflowID: number = -1;
export let app: ComfyApp = undefined; export let app: ComfyApp = undefined;
let layoutState: WritableLayoutStateStore = null;
let fileInput: HTMLInputElement = undefined;
let workflow: ComfyBoxWorkflow | null = null; let workflow: ComfyBoxWorkflow | null = null;
$: workflow = $workflowState.activeWorkflow; $: workflow = $workflowState.activeWorkflow;
@@ -22,111 +14,28 @@
navigator.vibrate(20) navigator.vibrate(20)
app.runDefaultQueueAction() app.runDefaultQueueAction()
} }
async function refreshCombos() {
navigator.vibrate(20)
await app.refreshComboInNodes()
}
function doSave(): void {
if (!fileInput)
return;
navigator.vibrate(20)
app.querySave()
}
function doLoad(): void {
if (!fileInput)
return;
navigator.vibrate(20)
fileInput.value = null;
fileInput.click();
}
function loadWorkflow(): void {
app.handleFile(fileInput.files[0]);
}
function doSaveLocal(): void {
navigator.vibrate(20)
app.saveStateToLocalStorage();
}
</script> </script>
<div class="bottom"> <Toolbar bottom color="red" style="bottom: calc(var(--f7-toolbar-height))">
{#if $queueState.runningNodeID || $queueState.progress}
<div class="node-name">
<span>Node: {getNodeInfo($queueState.runningNodeID)}</span>
</div>
<div class="progress-bar">
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} />
</div>
{/if}
{#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0}
<div class="queue-remaining in-progress">
<div>
Queued prompts: {$queueState.queueRemaining}.
</div>
</div>
{/if}
</div>
<Toolbar bottom>
{#if workflow != null && workflow.attrs.queuePromptButtonName != ""} {#if workflow != null && workflow.attrs.queuePromptButtonName != ""}
<div style:width="100%">
<Link on:click={queuePrompt}> <Link on:click={queuePrompt}>
{workflow.attrs.queuePromptButtonName} {workflow.attrs.queuePromptButtonName}
</Link> </Link>
</div>
{/if} {/if}
<Link on:click={refreshCombos}>🔄</Link>
<Link on:click={doSave}>Save</Link>
<Link on:click={doSaveLocal}>Save Local</Link>
<Link on:click={doLoad}>Load</Link>
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
</Toolbar> </Toolbar>
{#if $interfaceState.showIndicator}
<Indicator value={$interfaceState.indicatorValue} />
{/if}
<style lang="scss"> <style lang="scss">
#comfy-file-input { #comfy-file-input {
display: none; display: none;
} }
.bottom { :global(.toolbar) {
display: flex; --f7-toolbar-font-size: 13pt;
flex-direction: row;
position: absolute;
text-align: center;
width: 100%;
height: 2rem;
bottom: calc(var(--f7-toolbar-height) + var(--f7-safe-area-bottom));
z-index: var(--layer-top);
background-color: grey;
.node-name {
flex-grow: 1;
background-color: var(--color-red-300);
padding: 0.2em;
display: flex;
justify-content: center;
align-items: center;
} }
.progress-bar { :global(.dark .toolbar.color-red) {
flex-grow: 10; background: var(--neutral-700) !important;
background-color: var(--color-red-300);
display: flex;
justify-content: center;
align-items: center;
}
.queue-remaining {
flex-grow: 1;
padding: 0.2em;
&.in-progress {
background-color: var(--secondary-300);
}
}
} }
</style> </style>

View File

@@ -0,0 +1,198 @@
<script lang="ts">
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import queueState from "$lib/stores/queueState";
import workflowState, { ComfyBoxWorkflow } from "$lib/stores/workflowState";
import { getNodeInfo } from "$lib/utils"
import { LayoutTextSidebarReverse, Image, Grid } from "svelte-bootstrap-icons";
import { Link, Toolbar } from "framework7-svelte"
import ProgressBar from "$lib/components/ProgressBar.svelte";
import Progressbar from "$lib/components/f7/progressbar.svelte";
import Indicator from "./Indicator.svelte";
import interfaceState from "$lib/stores/interfaceState";
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
export let subworkflowID: number = -1;
export let app: ComfyApp = undefined;
let layoutState: WritableLayoutStateStore = null;
let fileInput: HTMLInputElement = undefined;
let workflow: ComfyBoxWorkflow | null = null;
$: workflow = $workflowState.activeWorkflow;
function queuePrompt() {
navigator.vibrate(20)
app.runDefaultQueueAction()
}
async function refreshCombos() {
navigator.vibrate(20)
await app.refreshComboInNodes()
}
function doSave(): void {
if (!fileInput)
return;
navigator.vibrate(20)
app.querySave()
}
function doLoad(): void {
if (!fileInput)
return;
navigator.vibrate(20)
fileInput.value = null;
fileInput.click();
}
function loadWorkflow(): void {
app.handleFile(fileInput.files[0]);
}
function doSaveLocal(): void {
navigator.vibrate(20)
app.saveStateToLocalStorage();
}
let queued: false;
$: queued = Boolean($queueState.runningNodeID || $queueState.progress)
let running = false;
$: running = typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0;
let progress;
$: progress = $queueState.progress
let progressPercent = 0
let progressText = ""
$: if (progress) {
progressPercent = (progress.value / progress.max) * 100;
progressText = progressPercent.toFixed(1) + "%";
} else {
progressPercent = 0
progressText = "??.?%"
}
let centerHref = "/workflows/"
$: if ($interfaceState.selectedWorkflowIndex && !$interfaceState.showingWorkflow) {
centerHref = `/workflows/${$interfaceState.selectedWorkflowIndex}/`
}
else {
centerHref = "/workflows/";
}
let toolbarCount = 0;
$: toolbarCount = $interfaceState.showingWorkflow ? 2 : 1;
const ICON_SIZE = "1.5rem";
let selectedTab = 1;
</script>
<div class="bottom" style:--toolbarCount={toolbarCount}>
<div class="bars">
{#if queued}
<div class="node-name">
<span>Node: {getNodeInfo($queueState.runningNodeID)} ({progressText})</span>
</div>
{/if}
</div>
<div class="wrapper">
{#if queued}
{#if progress}
<Progressbar color="blue" progress={progressPercent} />
{:else if running}
<Progressbar color="blue" infinite />
{/if}
{/if}
</div>
</div>
<Toolbar bottom tabbar color="blue" class={toolbarCount > 1 ? "hasGenToolbar" : ""}>
<Link transition="f7-dive" href="/queue/" tabLinkActive={$interfaceState.selectedTab === 0}>
<LayoutTextSidebarReverse width={ICON_SIZE} height={ICON_SIZE} />
</Link>
<Link transition="f7-dive" href={centerHref} tabLinkActive={$interfaceState.selectedTab === 1}>
<Image width={ICON_SIZE} height={ICON_SIZE} />
</Link>
<Link transition="f7-dive" href="/gallery/" tabLinkActive={$interfaceState.selectedTab === 2}>
<Grid width={ICON_SIZE} height={ICON_SIZE} />
</Link>
</Toolbar>
{#if $interfaceState.showIndicator}
<Indicator value={$interfaceState.indicatorValue} />
{/if}
<style lang="scss">
#comfy-file-input {
display: none;
}
:global(.progressbar.color-blue) {
background: var(--neutral-400) !important;
}
:global(.dark .progressbar.color-blue) {
background: var(--neutral-500) !important;
}
:global(.dark .toolbar.color-blue) {
background: var(--neutral-800) !important;
}
:global(.dark .toolbar.color-blue.hasGenToolbar) {
border-top: 2px solid var(--neutral-600);
}
:global(.dark .tab-link-active) {
--f7-tabbar-link-active-color: var(--secondary-500);
--f7-tabbar-link-active-bg-color: #283547;
}
.bottom {
--toolbarCount: 1;
position: absolute;
text-align: center;
width: 100%;
font-size: 13pt;
bottom: calc(var(--f7-toolbar-height) * var(--toolbarCount));
z-index: var(--layer-top);
}
.bars {
display: flex;
flex-direction: row;
.bars {
display: flex;
flex-direction: row;
}
.node-name {
flex-grow: 1;
background-color: var(--comfy-node-name-background);
color: var(--comfy-node-name-foreground);
padding: 0.2em;
display: flex;
justify-content: center;
align-items: center;
}
.progress-bar {
flex-grow: 10;
background-color: var(--color-red-300);
display: flex;
justify-content: center;
align-items: center;
}
.queue-remaining {
flex-grow: 1;
padding: 0.2em;
&.in-progress {
background-color: var(--secondary-300);
}
}
}
</style>

View File

@@ -0,0 +1,120 @@
<script lang="ts">
import { Page, Navbar, Block, Tabs, Tab, NavLeft, NavTitle, NavRight, Link, f7 } from "framework7-svelte"
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
import type ComfyApp from "$lib/components/ComfyApp";
import { writable, type Writable } from "svelte/store";
import type { IDragItem, WritableLayoutStateStore } from "$lib/stores/layoutStates";
import workflowState, { type ComfyBoxWorkflow, type WorkflowInstID } from "$lib/stores/workflowState";
import interfaceState from "$lib/stores/interfaceState";
import { onMount } from "svelte";
import GenToolbar from '../GenToolbar.svelte'
import { partition, showLightbox } from "$lib/utils";
import uiQueueState, { type QueueUIEntry } from "$lib/stores/uiQueueState";
import { showMobileLightbox } from "$lib/components/utils";
import notify from "$lib/notify";
export let app: ComfyApp
let _entries: ReadonlyArray<QueueUIEntry> = []
$: _entries = $uiQueueState.historyUIEntries;
let allEntries: [QueueUIEntry, string][][] = []
let allImages: string[] = []
let gridCols = 3;
function onPageBeforeIn() {
$interfaceState.selectedTab = 2;
}
$: buildImageList(_entries);
function buildImageList(entries: ReadonlyArray<QueueUIEntry>) {
const _allEntries = []
for (const entry of entries) {
for (const image of entry.images) {
_allEntries.push([entry, image]);
}
}
allEntries = partition(_allEntries, gridCols);
allImages = _allEntries.map(p => p[1]);
}
function handleClick(e: MouseEvent, entry: QueueUIEntry, index: number) {
showMobileLightbox(allImages, index)
}
async function clearHistory() {
f7.dialog.confirm("Are you sure you want to clear the current history?", async () => {
await app.clearQueue("history");
uiQueueState.updateEntries(true)
notify("History cleared!")
})
}
</script>
<Page name="gallery" on:pageBeforeIn={onPageBeforeIn}>
<Navbar>
<NavLeft></NavLeft>
<NavTitle>Gallery</NavTitle>
<NavRight>
<Link on:click={clearHistory}>🗑️</Link>
</NavRight>
</Navbar>
<Block>
{#each allEntries as group, i}
<div class="grid grid-cols-{gridCols} grid-gap">
{#each group as [entry, image], j}
{@const index = i * gridCols + j}
<div class="grid-entry">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<img class="grid-entry-image"
on:click={(e) => handleClick(e, entry, index)}
src={image}
loading="lazy"
alt="thumbnail" />
</div>
{/each}
</div>
{/each}
</Block>
</Page>
<style lang="scss">
.container {
overflow-x: hidden;
// Disable pull to refresh
overscroll-behavior-y: contain;
// framework7's css conflicts with gradio's
:global(.block) {
z-index: unset; // f7 sets it to 1
}
}
// TODO generalize this to all properties!
:global(.root-container.mobile > .block > .v-pane) {
flex-direction: column !important;
}
.grid-entry {
display: flex;
justify-content: center;
align-items: center;
aspect-ratio: 1 / 1;
object-fit: cover;
}
.grid-entry-image {
aspect-ratio: 1 / 1;
object-fit: cover;
margin-bottom: var(--f7-grid-gap);
&:hover {
cursor: pointer;
filter: brightness(120%) contrast(120%);
}
}
</style>

View File

@@ -1,35 +0,0 @@
<script lang="ts">
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Page, Navbar, Button, BlockTitle, Block, List, ListItem } from "framework7-svelte"
export let app: ComfyApp | null = null;
async function doLoadDefault() {
var confirmed = confirm("Would you like to load the default workflow in a new tab?");
if (confirmed) {
await app.initDefaultWorkflow();
}
}
</script>
<Page name="home">
<Navbar title="Home Page" />
<BlockTitle>Yo</BlockTitle>
<Block>
<div>{app} Nodes</div>
</Block>
<List strong inset dividersIos class="components-list searchbar-found">
<ListItem link="/subworkflows/" title="Workflows">
<i class="icon icon-f7" slot="media" />
</ListItem>
<ListItem link="/graph/" title="Show Node Graph">
<i class="icon icon-f7" slot="media" />
</ListItem>
</List>
<Block strong outlineIos>
<Button fill={true} onClick={doLoadDefault}>Load Default Graph</Button>
</Block>
</Page>

View File

@@ -1,16 +0,0 @@
<script lang="ts">
import ComfyApp from "$lib/components/ComfyApp";
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem } from "framework7-svelte"
export let app: ComfyApp;
</script>
<Page name="subworkflows">
<Navbar title="Workflows" backLink="Back" />
<List strong inset dividersIos class="components-list searchbar-found">
<ListItem link="/subworkflows/{1}/" title="Workflow 1">
<i class="icon icon-f7" slot="media" />
</ListItem>
</List>
</Page>

View File

@@ -0,0 +1,155 @@
<script lang="ts">
import { Page, Navbar, Block, Tabs, Tab, NavLeft, NavTitle, NavRight, Link, List, ListItem, Card, CardHeader, CardContent, CardFooter, f7 } from "framework7-svelte"
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
import type ComfyApp from "$lib/components/ComfyApp";
import { writable, type Writable } from "svelte/store";
import type { IDragItem, WritableLayoutStateStore } from "$lib/stores/layoutStates";
import workflowState, { type ComfyBoxWorkflow, type WorkflowInstID } from "$lib/stores/workflowState";
import interfaceState from "$lib/stores/interfaceState";
import { onMount } from "svelte";
import GenToolbar from '../GenToolbar.svelte'
import { partition, showLightbox, truncateString } from "$lib/utils";
import uiQueueState, { type QueueUIEntry } from "$lib/stores/uiQueueState";
import { showMobileLightbox } from "$lib/components/utils";
import queueState from "$lib/stores/queueState";
import notify from "$lib/notify";
export let app: ComfyApp
let _entries: ReadonlyArray<QueueUIEntry> = []
$: _entries = $uiQueueState.queueUIEntries;
function onPageBeforeIn() {
$interfaceState.selectedTab = 0;
}
async function interrupt() {
await app.interrupt();
}
async function clearQueue() {
f7.dialog.confirm("Are you sure you want to clear the current queue?", async () => {
await app.clearQueue("queue");
uiQueueState.updateEntries(true)
notify("Queue cleared!")
})
}
function findPrompt(entry: QueueUIEntry): string {
let s = ""
for (const inputs of Object.values(entry.entry.prompt)) {
if (inputs.class_type === "CLIPTextEncode") {
for (const [key, value] of Object.entries(inputs.inputs)) {
if (key === "text") {
s += value + "\n"
}
}
}
}
return truncateString(s, 240);
}
async function doCancel(entry: QueueUIEntry) {
if ($queueState.isInterrupting) {
return;
}
// TODO support interrupting from multiple running items!
if (entry.status === "running") {
await app.interrupt();
}
else {
await app.deleteQueueItem("queue", entry.entry.promptID);
}
notify("Queue item canceled.")
uiQueueState.updateEntries(true)
}
function getCardImage(entry: QueueUIEntry): string {
if (entry.images.length > 0)
return entry.images[0]
return "https://cdn.framework7.io/placeholder/nature-1000x600-3.jpg"
}
</script>
<Page name="queue" on:pageBeforeIn={onPageBeforeIn}>
<Navbar>
<NavLeft></NavLeft>
<NavTitle>Queue</NavTitle>
<NavRight>
<Link on:click={interrupt}>🛑️</Link>
<Link on:click={clearQueue}>🗑️</Link>
</NavRight>
</Navbar>
<Block>
<List>
{#each _entries as entry, i}
{@const prompt = findPrompt(entry)}
<ListItem>
<Card outlineMd class="demo-card-header-pic">
<CardHeader valign="bottom"
style="background-image: url({getCardImage(entry)})">
{entry.message}
</CardHeader>
<CardContent>
<p class="list-entry-message">{entry.submessage}</p>
<p>
{prompt}
</p>
</CardContent>
<CardFooter>
<Link on:click={() => doCancel(entry)}>Cancel</Link>
{#if entry.date}
<p>{entry.date}</p>
{/if}
</CardFooter>
</Card>
</ListItem>
{/each}
</List>
</Block>
</Page>
<style lang="scss">
.container {
overflow-x: hidden;
// Disable pull to refresh
overscroll-behavior-y: contain;
// framework7's css conflicts with gradio's
:global(.block) {
z-index: unset; // f7 sets it to 1
}
}
// TODO generalize this to all properties!
:global(.root-container.mobile > .block > .v-pane) {
flex-direction: column !important;
}
.grid-entry {
display: flex;
justify-content: center;
align-items: center;
aspect-ratio: 1 / 1;
object-fit: cover;
}
.grid-entry-image {
aspect-ratio: 1 / 1;
object-fit: cover;
margin-bottom: var(--f7-grid-gap);
&:hover {
cursor: pointer;
filter: brightness(120%) contrast(120%);
}
}
.list-entry-message {
font-size: 15pt;
}
</style>

View File

@@ -1,47 +0,0 @@
<script lang="ts">
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
import type ComfyApp from "$lib/components/ComfyApp";
import { writable, type Writable } from "svelte/store";
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
import workflowState, { type ComfyBoxWorkflow } from "$lib/stores/workflowState";
export let subworkflowID: number = -1;
export let app: ComfyApp
// TODO move
let workflow: ComfyBoxWorkflow | null = null
let layoutState: WritableLayoutStateStore | null = null;
$: workflow = $workflowState.activeWorkflow;
$: layoutState = workflow ? workflow.layout : null;
</script>
<Page name="subworkflow">
<Navbar title="Workflow {subworkflowID}" backLink="Back" />
{#if layoutState}
<div class="container">
<WidgetContainer bind:dragItem={$layoutState.root} {layoutState} isMobile={true} classes={["root-container", "mobile"]} />
</div>
{/if}
</Page>
<style lang="scss">
.container {
overflow-x: hidden;
// Disable pull to refresh
overscroll-behavior-y: contain;
// framework7's css conflicts with gradio's
:global(.block) {
z-index: unset; // f7 sets it to 1
}
}
// TODO generalize this to all properties!
:global(.root-container.mobile > .block > .v-pane) {
flex-direction: column !important;
}
</style>

View File

@@ -0,0 +1,114 @@
<script lang="ts">
import { Page, Navbar, Tabs, Tab, NavLeft, NavTitle, NavRight, Link, Actions, ActionsGroup, ActionsButton, ActionsLabel, Sheet, Toolbar, PageContent, Block } from "framework7-svelte"
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
import type ComfyApp from "$lib/components/ComfyApp";
import { writable, type Writable } from "svelte/store";
import { MenuUp } from 'svelte-bootstrap-icons';
import type { IDragItem, WritableLayoutStateStore } from "$lib/stores/layoutStates";
import workflowState, { type ComfyBoxWorkflow, type WorkflowInstID } from "$lib/stores/workflowState";
import interfaceState from "$lib/stores/interfaceState";
import { onMount } from "svelte";
import GenToolbar from '../GenToolbar.svelte'
import { onDestroy } from "svelte";
export let workflowIndex: number;
export let app: ComfyApp
let workflow: ComfyBoxWorkflow;
let root: IDragItem | null;
let title = ""
let actionsOpened = false;
function onPageBeforeIn() {
workflow = $workflowState.openedWorkflows[workflowIndex-1]
if (workflow) {
workflowState.setActiveWorkflow(app.lCanvas, workflow.id)
}
$interfaceState.selectedWorkflowIndex = workflowIndex
$interfaceState.showingWorkflow = true;
$interfaceState.selectedTab = 1;
}
function onPageBeforeOut() {
$interfaceState.showingWorkflow = false;
}
async function refreshCombos() {
navigator.vibrate(20)
await app.refreshComboInNodes()
}
function doSaveLocal(): void {
navigator.vibrate(20)
app.saveStateToLocalStorage();
}
$: layoutState = workflow?.layout;
$: title = workflow?.attrs?.title || `Workflow: ${workflow?.id || workflowIndex}`;
$: if (layoutState && $layoutState.root) {
root = $layoutState.root
} else {
root = null;
}
</script>
<Page name="workflow" style="--f7-page-toolbar-bottom-offset: calc(var(--f7-toolbar-height) * 2)"
on:pageBeforeIn={onPageBeforeIn}
on:pageBeforeOut={onPageBeforeOut}
>
<Navbar>
<NavLeft backLink="Back" backLinkUrl="/workflows/" backLinkForce={true}></NavLeft>
<NavTitle>{title}</NavTitle>
<NavRight>
<Link icon="icon-bars" on:click={() => {actionsOpened = true;}}>
<MenuUp />
</Link>
</NavRight>
</Navbar>
{#if workflow}
{#if root}
<div class="container">
<WidgetContainer bind:dragItem={root} isMobile={true} classes={["root-container"]} {layoutState} />
</div>
{/if}
{:else}
<div>
Workflow not found.
</div>
{/if}
<Actions bind:opened={actionsOpened}>
<ActionsGroup>
<ActionsLabel>Actions</ActionsLabel>
<ActionsButton strong on:click={refreshCombos}>Refresh Dropdowns</ActionsButton>
<ActionsButton strong on:click={doSaveLocal}>Save to Local Storage</ActionsButton>
<!-- <ActionsButton>Button 2</ActionsButton> -->
<ActionsButton color="red">Cancel</ActionsButton>
</ActionsGroup>
</Actions>
</Page>
<style lang="scss">
.container {
overflow-x: hidden;
// Disable pull to refresh
overscroll-behavior-y: contain;
// framework7's css conflicts with gradio's
:global(.block) {
z-index: unset; // f7 sets it to 1
}
}
// TODO generalize this to all properties!
:global(.root-container.mobile > .block > .v-pane) {
flex-direction: column !important;
}
.demo-sheet-push {
bottom: calc(var(--f7-toolbar-height) * 3);
}
</style>

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import workflowState, { ComfyBoxWorkflow, type WorkflowInstID } from "$lib/stores/workflowState";
import { onMount } from "svelte";
import interfaceState from "$lib/stores/interfaceState";
import { f7 } from 'framework7-svelte';
import { XCircle } from 'svelte-bootstrap-icons';
import { Page, Navbar, Button, BlockTitle, Block, List, ListItem } from "framework7-svelte"
export let app: ComfyApp | null = null;
let fileInput: HTMLInputElement = undefined;
async function doLoadDefault() {
f7.dialog.confirm("Would you like to load the default workflow in a new tab?", async () => {
await app.initDefaultWorkflow();
app.saveStateToLocalStorage(false);
})
}
function onClickDelete(workflow: ComfyBoxWorkflow, e: Event) {
e.preventDefault();
e.stopImmediatePropagation();
f7.dialog.confirm("Are you sure you want to delete this workflow?", workflow.attrs.title || `Workflow: ${workflow.id}`,
() => {
app.closeWorkflow(workflow.id);
app.saveStateToLocalStorage(false);
})}
function doLoad(): void {
if (!fileInput)
return;
navigator.vibrate(20)
fileInput.value = null;
fileInput.click();
}
function loadWorkflow(): void {
app.handleFile(fileInput.files[0]);
}
function onPageBeforeIn() {
$interfaceState.selectedWorkflowIndex = null;
$interfaceState.selectedTab = 1;
}
</script>
<Page name="home" on:pageBeforeIn={onPageBeforeIn}>
<Navbar title="Home Page" />
{#if $workflowState.openedWorkflows}
<List strong inset dividersIos class="components-list searchbar-found">
{#each $workflowState.openedWorkflows as workflow, i}
<ListItem link="/workflows/{i+1}/" transition="f7-cover" title={workflow.attrs.title || `Workflow: ${workflow.id}`}>
<svelte:fragment slot="media">
<div on:pointerdown={(e) => onClickDelete(workflow, e)}>
<XCircle width="1.5em" height="1.5em" />
</div>
</svelte:fragment>
</ListItem>
{/each}
</List>
{:else}
(No workflows opened.)
{/if}
<Block strong outlineIos>
<p class="grid grid-cols-2 grid-gap">
<Button outline onClick={doLoadDefault}>Load default graph</Button>
<Button outline onClick={doLoad}>Load from file...</Button>
</p>
</Block>
<input bind:this={fileInput} id="comfy-file-input" style:display="none" type="file" accept=".json" on:change={loadWorkflow} />
</Page>

View File

@@ -41,7 +41,7 @@ body {
--comfy-dropdown-item-background-active: var(--secondary-600); --comfy-dropdown-item-background-active: var(--secondary-600);
--comfy-progress-bar-background: var(--neutral-300); --comfy-progress-bar-background: var(--neutral-300);
--comfy-progress-bar-foreground: var(--secondary-300); --comfy-progress-bar-foreground: var(--secondary-300);
--comfy-node-name-background: var(--color-red-300); --comfy-node-name-background: var(--color-blue-200);
--comfy-node-name-foreground: var(--body-text-color); --comfy-node-name-foreground: var(--body-text-color);
--comfy-spinner-main-color: var(--neutral-400); --comfy-spinner-main-color: var(--neutral-400);
--comfy-spinner-accent-color: var(--secondary-500); --comfy-spinner-accent-color: var(--secondary-500);
@@ -77,11 +77,9 @@ body {
--comfy-node-name-foreground: var(--body-text-color); --comfy-node-name-foreground: var(--body-text-color);
--comfy-spinner-main-color: var(--neutral-600); --comfy-spinner-main-color: var(--neutral-600);
--comfy-spinner-accent-color: var(--secondary-600); --comfy-spinner-accent-color: var(--secondary-600);
}
.mobile { --f7-navbar-color: var(--body-text-color);
--comfy-progress-bar-background: lightgrey; --f7-navbar-bg-color: var(--neutral-800);
--comfy-progress-bar-foreground: #B3D8A9
} }
@mixin square-button { @mixin square-button {
@@ -252,3 +250,15 @@ button {
:global([data-is-dnd-shadow-item]) { :global([data-is-dnd-shadow-item]) {
min-height: 5rem; min-height: 5rem;
} }
:global(.dark .photo-browser-popup) {
background: var(--neutral-700);
}
:global(.dark .photo-browser-popup-) {
background: var(--neutral-700);
}
:global(.photo-browser-exposed .toolbar ~ .toolbar.photo-browser-thumbs) {
transform: translate3d(0, calc(var(--f7-toolbar-height) * 2), 0);
}

View File

@@ -55,6 +55,7 @@ export default defineConfig({
}, },
}, },
build: { build: {
minify: isProduction,
sourcemap: true, sourcemap: true,
rollupOptions: { rollupOptions: {
input: { input: {