diff --git a/index.html b/index.html index 28202f3..b628d53 100644 --- a/index.html +++ b/index.html @@ -7,17 +7,6 @@ -
diff --git a/package.json b/package.json index 670e59f..a258134 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "prettier-plugin-svelte": "^2.10.0", "rollup-plugin-visualizer": "^5.9.0", "sass": "^1.61.0", - "svelte": "^3.58.0", + "svelte": "^3.59.0", "svelte-check": "^3.2.0", "svelte-dnd-action": "^0.9.22", "typescript": "^5.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f8345e..b3a1120 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: version: 6.11.0 '@dogagenc/svelte-markdown': specifier: ^0.2.4 - version: 0.2.4(svelte@3.58.0) + version: 0.2.4(svelte@3.59.1) '@gradio/accordion': specifier: workspace:* version: link:gradio/js/accordion @@ -93,7 +93,7 @@ importers: version: link:litegraph/packages/tsconfig '@sveltejs/vite-plugin-svelte': 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': specifier: ^4.0.1 version: 4.0.1 @@ -156,13 +156,13 @@ importers: version: 1.5.2 svelte-preprocess: 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: specifier: ^5.5.3 version: 5.5.3 svelte-splitpanes: specifier: ^0.7.13 - version: 0.7.13(svelte@3.58.0) + version: 0.7.13(svelte@3.59.1) svelte-tiny-virtual-list: specifier: ^2.0.5 version: 2.0.5 @@ -190,7 +190,7 @@ importers: version: 1.2.8 '@zerodevx/svelte-toast': specifier: ^0.9.3 - version: 0.9.3(svelte@3.58.0) + version: 0.9.3(svelte@3.59.1) eslint: specifier: ^8.37.0 version: 8.37.0 @@ -199,7 +199,7 @@ importers: version: 8.8.0(eslint@8.37.0) eslint-plugin-svelte3: 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: specifier: ^9.18.3 version: 9.18.3 @@ -211,7 +211,7 @@ importers: version: 2.8.7 prettier-plugin-svelte: 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: specifier: ^5.9.0 version: 5.9.0 @@ -219,14 +219,14 @@ importers: specifier: ^1.61.0 version: 1.61.0 svelte: - specifier: ^3.58.0 - version: 3.58.0 + specifier: ^3.59.0 + version: 3.59.1 svelte-check: 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: specifier: ^0.9.22 - version: 0.9.22(svelte@3.58.0) + version: 0.9.22(svelte@3.59.1) typescript: specifier: ^5.0.3 version: 5.0.3 @@ -1363,6 +1363,7 @@ packages: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.6.0 '@codemirror/state': 6.2.0 @@ -1506,14 +1507,14 @@ packages: w3c-keyname: 2.2.6 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==} peerDependencies: svelte: ^3.0.0 dependencies: '@types/marked': 4.3.1 marked: 4.3.0 - svelte: 3.58.0 + svelte: 3.59.1 dev: false /@esbuild/android-arm64@0.17.18: @@ -2275,7 +2276,7 @@ packages: - supports-color 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==} engines: {node: ^14.18.0 || >= 16} peerDependencies: @@ -2286,8 +2287,8 @@ packages: deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.0 - svelte: 3.58.0 - svelte-hmr: 0.15.1(svelte@3.58.0) + svelte: 3.59.1 + svelte-hmr: 0.15.1(svelte@3.59.1) vite: 4.3.8(sass@1.61.0) vitefu: 0.2.4(vite@4.3.8) transitivePeerDependencies: @@ -2604,12 +2605,12 @@ packages: pretty-format: 27.5.1 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==} peerDependencies: svelte: ^3.57.0 dependencies: - svelte: 3.58.0 + svelte: 3.59.1 dev: true /abab@2.0.6: @@ -3117,6 +3118,8 @@ packages: '@codemirror/search': 6.4.0 '@codemirror/state': 6.2.0 '@codemirror/view': 6.11.0 + transitivePeerDependencies: + - '@lezer/common' dev: false /codemirror@6.0.1(@lezer/common@1.0.2): @@ -3941,14 +3944,14 @@ packages: - typescript 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==} peerDependencies: eslint: '>=8.0.0' svelte: ^3.2.0 dependencies: eslint: 8.37.0 - svelte: 3.58.0 + svelte: 3.59.1 dev: true /eslint-scope@5.1.1: @@ -5988,14 +5991,14 @@ packages: engines: {node: '>= 0.8.0'} 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==} peerDependencies: prettier: ^1.16.4 || ^2.0.0 svelte: ^3.2.0 dependencies: prettier: 2.8.7 - svelte: 3.58.0 + svelte: 3.59.1 dev: true /prettier@2.8.7: @@ -6656,7 +6659,7 @@ packages: - sugarss 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==} hasBin: true peerDependencies: @@ -6668,8 +6671,8 @@ packages: import-fresh: 3.3.0 picocolors: 1.0.0 sade: 1.8.1 - svelte: 3.58.0 - svelte-preprocess: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3) + svelte: 3.59.1 + svelte-preprocess: 5.0.3(sass@1.61.0)(svelte@3.59.1)(typescript@5.0.3) typescript: 5.0.3 transitivePeerDependencies: - '@babel/core' @@ -6683,18 +6686,18 @@ packages: - sugarss 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==} peerDependencies: svelte: '>=3.23.0' dependencies: - svelte: 3.58.0 + svelte: 3.59.1 dev: true /svelte-feather-icons@4.0.0: resolution: {integrity: sha512-4ieUsjp+VYa1r6y80jDt9zRiRUZyJNbESpRdHdJJhiBubyuXX96A7f1UZSK4olxzP6Qsg5ZAuyZlnmvD+/swAA==} dependencies: - svelte: 3.58.0 + svelte: 3.59.1 dev: false /svelte-floating-ui@1.2.8: @@ -6718,6 +6721,16 @@ packages: svelte: '>=3.19.0' dependencies: 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): resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==} @@ -6825,7 +6838,7 @@ packages: typescript: 5.0.3 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==} engines: {node: '>= 14.10.0'} requiresBuild: true @@ -6869,7 +6882,7 @@ packages: sass: 1.61.0 sorcery: 0.11.0 strip-indent: 3.0.0 - svelte: 3.58.0 + svelte: 3.59.1 typescript: 5.0.3 /svelte-range-slider-pips@2.0.1: @@ -6883,13 +6896,13 @@ packages: svelte-floating-ui: 1.2.8 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==} peerDependencies: svelte: ^3.54.0 dependencies: esm-env-robust: 0.0.3 - svelte: 3.58.0 + svelte: 3.59.1 dev: false /svelte-tiny-virtual-list@2.0.5: @@ -6912,6 +6925,10 @@ packages: resolution: {integrity: sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==} engines: {node: '>= 8'} + /svelte@3.59.1: + resolution: {integrity: sha512-pKj8fEBmqf6mq3/NfrB9SLtcJcUvjYSWyePlfCqN9gujLB25RitWK8PvFzlwim6hD/We35KbPlRteuA6rnPGcQ==} + engines: {node: '>= 8'} + /swiper@9.2.4: resolution: {integrity: sha512-L7y3K/iiMXNYQ94FbfcJn7jex4QPnS4+voXGupTdC+UHW4XrR40QDdm4c9hXJ+Br0Il7PP0vP1W3goM9/Ly6Sg==} engines: {node: '>= 4.7.0'} diff --git a/src/App.svelte b/src/App.svelte index 11b98f9..8aed45d 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -4,12 +4,12 @@ import "@litegraph-ts/core/css/litegraph.css"; import "./scss/global.scss"; - import { onMount } from 'svelte'; - export let app: ComfyAppState; + export let isMobile: boolean - - - +{#if isMobile} +
Redirecting...
+{:else} + +{/if} diff --git a/src/AppMobile.svelte b/src/AppMobile.svelte index a120c51..6081b20 100644 --- a/src/AppMobile.svelte +++ b/src/AppMobile.svelte @@ -2,22 +2,22 @@ import { onMount } from "svelte"; 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 "framework7/css/bundle" import "./scss/global.scss"; + import MainToolbar from './mobile/MainToolbar.svelte' import GenToolbar from './mobile/GenToolbar.svelte' - import HomePage from './mobile/routes/home.svelte'; - import AboutPage from './mobile/routes/about.svelte'; - import LoginPage from './mobile/routes/login.svelte'; - import GraphPage from './mobile/routes/graph.svelte'; - import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte'; - import SubWorkflowPage from './mobile/routes/subworkflow.svelte'; + import WorkflowsPage from './mobile/routes/workflows.svelte'; + import QueuePage from './mobile/routes/queue.svelte'; + import GalleryPage from './mobile/routes/gallery.svelte'; + import WorkflowPage from './mobile/routes/workflow.svelte'; import type { Framework7Parameters, Modal } from "framework7/types"; + import interfaceState from "$lib/stores/interfaceState"; export let app: ComfyApp; @@ -51,11 +51,40 @@ } } + let appSetupPromise: Promise = null; + let loading = true; + let lastSize = Number.POSITIVE_INFINITY; + + $: f7 && f7.setDarkMode($interfaceState.isDarkMode) + 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("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. @@ -66,36 +95,42 @@ routes: [ { path: '/', - component: HomePage, + component: WorkflowsPage, options: { props: { app } } }, { - path: '/about/', - component: AboutPage, - }, - { - path: '/login/', - component: LoginPage, - }, - { - path: '/graph/', - component: GraphPage, + path: '/workflows', + component: WorkflowsPage, options: { props: { app } } }, { - path: '/subworkflows/', - component: ListSubWorkflowsPage, + path: '/queue/', + component: QueuePage, options: { props: { app } } }, { - path: '/subworkflows/:subworkflowID/', - component: SubWorkflowPage, + path: '/gallery/', + component: GalleryPage, + options: { + props: { app } + } + }, + // { + // path: '/graph/', + // component: GraphPage, + // options: { + // props: { app } + // } + // }, + { + path: '/workflows/:workflowIndex/', + component: WorkflowPage, options: { props: { app } } @@ -113,23 +148,90 @@ actions: { 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); -{#if app} - - - - - - -{/if} + + + + {#if appSetupPromise} + {#await appSetupPromise} +
+
+ +
+
+ {:then} + + + {#if $interfaceState.selectedWorkflowIndex && $interfaceState.showingWorkflow} + + {/if} + + {:catch error} +
+
+ Error loading app +
+
{error}
+ {#if error != null && error.stack} + {@const lines = error.stack.split("\n")} + {#each lines as line} +
{line}
+ {/each} + {/if} +
+ {/await} + {/if} +
+ + + diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index ae686bb..220d85e 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -333,7 +333,8 @@ export default class ComfyGraphCanvas extends LGraphCanvas { /** * 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 { const res = super.processKey(e); @@ -353,7 +354,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { } if (e.type == "keydown") { - // Ctrl + M mute/unmute + // Ctrl + M - mute/unmute if (e.keyCode == 77 && e.ctrlKey) { if (this.selected_nodes) { for (var i in this.selected_nodes) { @@ -366,6 +367,21 @@ export default class ComfyGraphCanvas extends LGraphCanvas { } 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(); diff --git a/src/lib/api.ts b/src/lib/api.ts index 7d7f47d..dc8765b 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -125,8 +125,17 @@ export default class ComfyAPI { }, 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 { - const hostname = this.hostname || location.hostname; + const hostname = this.getHostname() const port = this.port || location.port; return `${window.location.protocol}//${hostname}:${port}` } @@ -146,7 +155,7 @@ export default class ComfyAPI { existingSession = "?clientId=" + existingSession; } - const hostname = this.hostname || location.hostname; + const hostname = this.getHostname() const port = this.port || location.port; this.socket = new WebSocket( diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 6eb6d7f..2c77775 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -211,7 +211,13 @@ export default class ComfyApp { this.lCanvas.allow_interaction = uiUnlocked; // 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); // Load previous workflow @@ -362,7 +368,7 @@ export default class ComfyApp { } } - saveStateToLocalStorage() { + saveStateToLocalStorage(doNotify: boolean = true) { try { uiState.update(s => { s.forceSaveUserState = true; return s; }) const state = get(workflowState) @@ -374,10 +380,12 @@ export default class ComfyApp { for (const workflow of workflows) workflow.isModified = false; workflowState.set(get(workflowState)); - notify("Saved to local storage.") + if (doNotify) + notify("Saved to local storage.") } catch (err) { - notify(`Failed saving to local storage:\n${err}`, { type: "error" }) + if (doNotify) + notify(`Failed saving to local storage:\n${err}`, { type: "error" }) } finally { uiState.update(s => { s.forceSaveUserState = null; return s; }) @@ -395,6 +403,9 @@ export default class ComfyApp { return false; const workflows = state.workflows as SerializedAppState[]; + if (workflows.length === 0) + return false; + await Promise.all(workflows.map(w => { return this.openWorkflow(w, { refreshCombos: defs, warnMissingNodeTypes: false, setActive: false }).catch(error => { console.error("Failed restoring previous workflow", error) diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte index e71a73e..2b8c40d 100644 --- a/src/lib/components/ComfyQueue.svelte +++ b/src/lib/components/ComfyQueue.svelte @@ -30,6 +30,7 @@ import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte"; import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte"; import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte"; + import uiQueueState from "$lib/stores/uiQueueState"; export let app: ComfyApp; @@ -52,46 +53,34 @@ let displayMode: DisplayModeType = "list"; let imageSize: number = 40; let gridColumns: number = 3; - let changed = true; function switchMode(newMode: QueueItemType) { - changed = mode !== newMode + const changed = mode !== newMode mode = newMode if (changed) { - _queuedEntries = [] - _runningEntries = [] - _entries = [] + uiQueueState.updateEntries(); } } function switchDisplayMode(newDisplayMode: DisplayModeType) { - // changed = displayMode !== newDisplayMode displayMode = newDisplayMode - // if (changed) { - // _queuedEntries = [] - // _runningEntries = [] - // _entries = [] - // } } - let _queuedEntries: QueueUIEntry[] = [] - let _runningEntries: QueueUIEntry[] = [] - let _entries: QueueUIEntry[] = [] - - $: if (mode === "queue" && (changed || $queuePending.length != _queuedEntries.length || $queueRunning.length != _runningEntries.length)) { + let _entries: ReadonlyArray = [] + $: if(mode === "queue") { + _entries = $uiQueueState.queueUIEntries updateFromQueue(); - changed = false; } - else if (mode === "history" && (changed || $queueCompleted.length != _entries.length)) { + else { + _entries = $uiQueueState.historyUIEntries; updateFromHistory(); - changed = false; } $: if (mode === "queue" && !$queuePending && !$queueRunning) { - _queuedEntries = [] - _runningEntries = [] - _entries = []; - changed = true + uiQueueState.clearQueue(); + } + else if (mode === "history" && !$queueCompleted) { + uiQueueState.clearHistory(); } async function deleteEntry(entry: QueueUIEntry, event: MouseEvent) { @@ -106,125 +95,26 @@ await app.deleteQueueItem(mode, entry.entry.promptID); } - if (mode === "queue") { - _queuedEntries = [] - _runningEntries = [] - } - - _entries = []; - changed = true; + uiQueueState.updateEntries(true) } async function clearQueue() { await app.clearQueue(mode); - - 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; + uiQueueState.updateEntries(true) } 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) { await tick(); // Wait for list size to be recalculated queueList.scroll({ top: queueList.scrollHeight }) } - console.warn("[ComfyQueue] BUILDQUEUE", _entries.length, $queuePending.length, $queueRunning.length) } async function updateFromHistory() { - _entries = $queueCompleted.map(convertCompletedEntry).reverse(); if (queueList) { + await tick(); // Wait for list size to be recalculated queueList.scrollTo(0, 0); } - console.warn("[ComfyQueue] BUILDHISTORY", _entries.length, $queueCompleted.length) } async function interrupt() { diff --git a/src/lib/components/f7/progressbar.svelte b/src/lib/components/f7/progressbar.svelte new file mode 100644 index 0000000..367f7ef --- /dev/null +++ b/src/lib/components/f7/progressbar.svelte @@ -0,0 +1,58 @@ + + + + + + diff --git a/src/lib/components/utils.ts b/src/lib/components/utils.ts index 3a3c8dd..b8d3d6b 100644 --- a/src/lib/components/utils.ts +++ b/src/lib/components/utils.ts @@ -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 { LGraphCanvas, Vector2 } from "@litegraph-ts/core"; 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>): IDragItem[] { return layoutState.updateChildren(container, evt.detail.items) @@ -45,3 +48,25 @@ function doInsertTemplate(layoutState: WritableLayoutStateStore, droppedTemplate return get(layoutState).allItems[container.id].children; } + +let mobileLightbox = null; + +export function showMobileLightbox(images: any[], selectedImage: number, options: Partial = {}) { + 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) +} diff --git a/src/lib/nodes/actions/ComfySendOutputAction.ts b/src/lib/nodes/actions/ComfySendOutputAction.ts index 1f6fece..7b744b2 100644 --- a/src/lib/nodes/actions/ComfySendOutputAction.ts +++ b/src/lib/nodes/actions/ComfySendOutputAction.ts @@ -8,6 +8,7 @@ import notify from "$lib/notify"; import workflowState from "$lib/stores/workflowState"; import { get } from "svelte/store"; import type ComfyApp from "$lib/components/ComfyApp"; +import interfaceState from "$lib/stores/interfaceState"; export interface ComfySendOutputActionProperties extends ComfyGraphNodeProperties { } @@ -41,36 +42,8 @@ export default class ComfySendOutputAction extends ComfyGraphNode { this.isActive = true; - const doSend = (modal: ModalData) => { + interfaceState.querySendOutput(value, type, receiveTargets, () => { 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 }) }; } diff --git a/src/lib/nodes/widgets/ComfyMarkdownNode.ts b/src/lib/nodes/widgets/ComfyMarkdownNode.ts index 8d41bf5..27de2f0 100644 --- a/src/lib/nodes/widgets/ComfyMarkdownNode.ts +++ b/src/lib/nodes/widgets/ComfyMarkdownNode.ts @@ -42,6 +42,7 @@ export default class ComfyMarkdownNode extends ComfyWidgetNode { }, { multiline: true, + inputStyle: { fontFamily: "monospace" } } ) diff --git a/src/lib/notify.ts b/src/lib/notify.ts index 3de3405..80a0f2c 100644 --- a/src/lib/notify.ts +++ b/src/lib/notify.ts @@ -27,6 +27,11 @@ function notifyf7(text: string, options: NotifyOptions) { on.click = () => options.onClick(); } + let icon = null; + if (options.imageUrl) { + icon = `` + } + const notification = f7.notification.create({ title: options.title, titleRightText: 'now', @@ -34,7 +39,8 @@ function notifyf7(text: string, options: NotifyOptions) { text: text, closeOnClick: true, closeTimeout, - on + on, + icon }); notification.open(); } diff --git a/src/lib/stores/configState.ts b/src/lib/stores/configState.ts index e6fe1e9..75069b1 100644 --- a/src/lib/stores/configState.ts +++ b/src/lib/stores/configState.ts @@ -24,7 +24,12 @@ let changedOptions: Partial> = {} function getBackendURL(): string { 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 { diff --git a/src/lib/stores/interfaceState.ts b/src/lib/stores/interfaceState.ts index c792dd5..ec1c04a 100644 --- a/src/lib/stores/interfaceState.ts +++ b/src/lib/stores/interfaceState.ts @@ -1,6 +1,12 @@ -import { debounce } from '$lib/utils'; +import { debounce, isMobileBrowser } from '$lib/utils'; import { get, 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 = { // Show a large indicator of the currently editing number value for mobile @@ -10,12 +16,20 @@ export type InterfaceState = { showIndicator: boolean, indicatorValue: any, - graphTransitioning: boolean - isJumpingToNode: boolean + graphTransitioning: boolean, + isJumpingToNode: boolean, + + selectedWorkflowIndex: number | null + showingWorkflow: boolean, + selectedTab: number, + showSheet: boolean, + + isDarkMode: boolean } type InterfaceStateOps = { showIndicator: (pointerX: number, pointerY: number, value: any) => void, + querySendOutput: (value: any, type: SlotType, receiveTargets: WorkflowReceiveOutputTargets[], cb: (modal: ModalData) => void) => void, } export type WritableInterfaceStateStore = Writable & InterfaceStateOps; @@ -28,6 +42,13 @@ const store: Writable = writable( graphTransitioning: 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) @@ -46,9 +67,49 @@ function showIndicator(pointerX: number, pointerY: number, value: any) { 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 = { ...store, - showIndicator + showIndicator, + querySendOutput } export default interfaceStateStore; diff --git a/src/lib/stores/queueState.ts b/src/lib/stores/queueState.ts index 56cf239..f50048a 100644 --- a/src/lib/stores/queueState.ts +++ b/src/lib/stores/queueState.ts @@ -8,6 +8,7 @@ import { get, writable, type Writable } from "svelte/store"; import { v4 as uuidv4 } from "uuid"; import workflowState, { type WorkflowError, type WorkflowExecutionError, type WorkflowInstID, type WorkflowValidationError } from "./workflowState"; import configState from "./configState"; +import uiQueueState from "./uiQueueState"; export type QueueEntryStatus = "success" | "validation_failed" | "error" | "interrupted" | "all_cached" | "unknown"; @@ -444,7 +445,6 @@ function queueCleared(type: QueueItemType) { store.update(s => { if (type === "queue") { s.queuePending.set([]); - s.queueRunning.set([]); s.queueRemaining = 0; s.runningNodeID = null; s.progress = null; diff --git a/src/lib/stores/uiQueueState.ts b/src/lib/stores/uiQueueState.ts new file mode 100644 index 0000000..a361c22 --- /dev/null +++ b/src/lib/stores/uiQueueState.ts @@ -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 & UIQueueStateOps; +const store: Writable = 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; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e340256..68bdff3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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 { get } from "svelte/store"; import type { ComfyNodeID } from "./api"; -import { type SerializedPrompt } from "./components/ComfyApp"; -import workflowState from "./stores/workflowState"; +import ComfyApp, { type SerializedPrompt } from "./components/ComfyApp"; +import workflowState, { type WorkflowReceiveOutputTargets } from "./stores/workflowState"; import { ImageViewer } from "./ImageViewer"; 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 { if (max <= min) @@ -618,6 +619,9 @@ export async function readFileToText(file: File): Promise { reader.onload = async () => { resolve(reader.result as string); }; + reader.onerror = async () => { + reject(reader.error); + } reader.readAsText(file); }) } @@ -709,3 +713,31 @@ export function canvasToBlob(canvas: HTMLCanvasElement): Promise { canvas.toBlob(resolve); }); } + +export type SafetensorsMetadata = Record + +export async function getSafetensorsMetadata(folder: string, filename: string): Promise { + 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(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)) +} diff --git a/src/lib/widgets/ComboWidget.svelte b/src/lib/widgets/ComboWidget.svelte index 63816c1..672f926 100644 --- a/src/lib/widgets/ComboWidget.svelte +++ b/src/lib/widgets/ComboWidget.svelte @@ -8,6 +8,7 @@ import { type WidgetLayout } from "$lib/stores/layoutStates"; import { get, writable, type Writable } from "svelte/store"; import { isDisabled } from "./utils" + import { getSafetensorsMetadata } from '$lib/utils'; export let widget: WidgetLayout | null = null; export let isMobile: boolean = false; let node: ComfyComboNode | null = null; @@ -73,26 +74,10 @@ } } - function onSelect(e: CustomEvent) { - 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 hoverItemIndex = null; let filterText = ""; let listOpen = null; - let scrollToIndex = null; - let start = 0; - let end = 0; function handleHover(index: number) { // console.warn("HOV", index) @@ -107,7 +92,9 @@ $nodeValue = item.value listOpen = false; filterText = "" - input?.blur() + setTimeout(() => { + input?.blur(); + }, 100) } function onFilter() { @@ -173,7 +160,10 @@ on:select={(e) => handleSelect(e.detail.index)} on:blur on:filter={onFilter}> -
+
{#if filteredItems.length > 0} {@const itemSize = isMobile ? 50 : 25} {@const itemsToShow = isMobile ? 10 : 30} @@ -291,9 +281,14 @@ .comfy-select-list { --maxLabelWidth: 100; + --maxListWidth: 50vw; + &.mobile { + --maxListWidth: 80vw; + } + font-size: 14px; - width: min(calc((var(--maxLabelWidth) + 10) * 1ch), 50vw); color: var(--item-color); + width: min(calc((var(--maxLabelWidth) + 10) * 1ch), var(--maxListWidth)); > :global(.virtual-list-wrapper) { box-shadow: var(--block-shadow); diff --git a/src/lib/widgets/GalleryWidget.svelte b/src/lib/widgets/GalleryWidget.svelte index 7873448..c820d34 100644 --- a/src/lib/widgets/GalleryWidget.svelte +++ b/src/lib/widgets/GalleryWidget.svelte @@ -11,6 +11,7 @@ import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils"; import { f7 } from "framework7-svelte"; import type { ComfyGalleryNode } from "$lib/nodes/widgets"; + import { showMobileLightbox } from "$lib/components/utils"; export let widget: WidgetLayout | null = null; export let isMobile: boolean = false; @@ -47,19 +48,8 @@ object_fit: "cover", // 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("div.block") console.debug("[ImageViewer] showModal", source, galleryElem); if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) { @@ -72,23 +62,16 @@ const images = allGalleryButtons.map(button => { return { url: (button.children[0] as HTMLImageElement).src, - caption: "Image" + // caption: "Image" } }) - history.pushState({ type: "gallery" }, ""); - - mobileLightbox = f7.photoBrowser.create({ - photos: images, - thumbs: images.map(i => i.url), - type: 'popup', - }); - mobileLightbox.open($selected_image) + showMobileLightbox(images, selectedImage, { thumbs: images }); } function onClicked(e: CustomEvent) { if (isMobile) { - showMobileLightbox(e.detail) + showMobileLightbox_(e.detail, $selected_image) } else { ImageViewer.instance.showLightbox(e.detail) @@ -103,7 +86,7 @@ {#if widget && node && nodeValue && $nodeValue} {#if widget.attrs.variant === "image"} -
+
{#if $nodeValue && $nodeValue.length > 0} {@const value = $nodeValue[$nodeValue.length-1]} @@ -122,7 +105,7 @@
{:else} {@const images = $nodeValue.map(comfyBoxImageToComfyURL)} -