Improve mobile app initialization and range widget corner indicator
This commit is contained in:
@@ -1,17 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { Button } from "@gradio/button";
|
||||
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import { download } from "$lib/utils"
|
||||
|
||||
import { LGraph, LGraphNode } from "@litegraph-ts/core";
|
||||
import type { ComfyAPIStatus } from "$lib/api";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import { App, View, Toolbar, Page, Navbar, Link, BlockTitle, Block, List, ListItem } from "framework7-svelte"
|
||||
import { App, View } from "framework7-svelte"
|
||||
|
||||
import { f7, f7ready } from 'framework7-svelte';
|
||||
|
||||
@@ -27,6 +18,9 @@
|
||||
import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte';
|
||||
import SubWorkflowPage from './mobile/routes/subworkflow.svelte';
|
||||
import HellPage from './mobile/routes/hell.svelte';
|
||||
import type { Framework7Parameters } from "framework7/types";
|
||||
|
||||
export let app: ComfyApp;
|
||||
|
||||
function onBackKeyDown(e) {
|
||||
if(f7.view.current.router.currentRoute.path == '/'){
|
||||
@@ -40,6 +34,8 @@
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await app.setup();
|
||||
(window as any).app = app;
|
||||
window.addEventListener("backbutton", onBackKeyDown, false);
|
||||
window.addEventListener("popstate", onBackKeyDown, false);
|
||||
});
|
||||
@@ -49,11 +45,14 @@
|
||||
We need to pass them along with the F7 app parameters to <App> component
|
||||
*/
|
||||
|
||||
let f7params = {
|
||||
let f7params: Framework7Parameters = {
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: HomePage,
|
||||
options: {
|
||||
props: { app }
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/about/',
|
||||
@@ -66,18 +65,30 @@
|
||||
{
|
||||
path: '/graph/',
|
||||
component: GraphPage,
|
||||
options: {
|
||||
props: { app }
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/subworkflows/',
|
||||
component: ListSubWorkflowsPage,
|
||||
options: {
|
||||
props: { app }
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/subworkflows/:subworkflowID/',
|
||||
component: SubWorkflowPage,
|
||||
options: {
|
||||
props: { app }
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/hell/',
|
||||
component: HellPage,
|
||||
options: {
|
||||
props: { app }
|
||||
}
|
||||
},
|
||||
],
|
||||
popup: {
|
||||
@@ -105,7 +116,10 @@
|
||||
browserHistory=true,
|
||||
browserHistoryRoot="/mobile/"
|
||||
>
|
||||
<GenToolbar/>
|
||||
<GenToolbar {app} />
|
||||
</View>
|
||||
</App>
|
||||
<div class="canvas-wrapper pane-wrapper" style="display: none">
|
||||
<canvas id="graph-canvas" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -11,7 +11,7 @@ export type SerializedGraphCanvasState = {
|
||||
}
|
||||
|
||||
export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
app: ComfyApp
|
||||
app: ComfyApp | null;
|
||||
|
||||
constructor(
|
||||
app: ComfyApp,
|
||||
@@ -60,7 +60,8 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
let color = null;
|
||||
if (node.id === +state.runningNodeId) {
|
||||
color = "#0f0";
|
||||
} else if (this.app.dragOverNode && node.id === this.app.dragOverNode.id) {
|
||||
// this.app can be null inside the constructor if rendering is taking place already
|
||||
} else if (this.app && this.app.dragOverNode && node.id === this.app.dragOverNode.id) {
|
||||
color = "dodgerblue";
|
||||
}
|
||||
|
||||
|
||||
@@ -175,10 +175,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
app.api.addEventListener("status", (ev: CustomEvent) => {
|
||||
queueState.statusUpdated(ev.detail as ComfyAPIStatus);
|
||||
});
|
||||
|
||||
$: if (app.rootEl && !imageViewer) {
|
||||
imageViewer = new ImageViewer(app.rootEl);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType } from "@litegraph-ts/core";
|
||||
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
||||
import ComfyAPI from "$lib/api"
|
||||
import ComfyAPI, { type ComfyAPIQueueStatus } from "$lib/api"
|
||||
import defaultGraph from "$lib/defaultGraph"
|
||||
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||
import EventEmitter from "events";
|
||||
@@ -293,6 +293,10 @@ export default class ComfyApp {
|
||||
this.lGraph.setDirtyCanvas(true, false);
|
||||
});
|
||||
|
||||
this.api.addEventListener("status", (ev: CustomEvent) => {
|
||||
queueState.statusUpdated(ev.detail as ComfyAPIQueueStatus);
|
||||
});
|
||||
|
||||
this.api.addEventListener("executed", ({ detail }: CustomEvent) => {
|
||||
this.nodeOutputs[detail.node] = detail.output;
|
||||
const node = this.lGraph.getNodeById(detail.node) as ComfyGraphNode;
|
||||
|
||||
@@ -74,6 +74,9 @@
|
||||
{step}
|
||||
{disabled}
|
||||
on:mouseup={handle_release}
|
||||
on:pointerdown
|
||||
on:pointerup
|
||||
on:pointermove
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
48
src/lib/stores/interfaceState.ts
Normal file
48
src/lib/stores/interfaceState.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { debounce } from '$lib/utils';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
|
||||
export type InterfaceState = {
|
||||
// Show a large indicator of the currently editing number value for mobile
|
||||
// use (sliders).
|
||||
pointerNearTop: boolean,
|
||||
pointerNearLeft: boolean,
|
||||
showIndicator: boolean,
|
||||
indicatorValue: any,
|
||||
}
|
||||
|
||||
type InterfaceStateOps = {
|
||||
showIndicator: (pointerX: number, pointerY: number, value: any) => void,
|
||||
}
|
||||
|
||||
export type WritableInterfaceStateStore = Writable<InterfaceState> & InterfaceStateOps;
|
||||
const store: Writable<InterfaceState> = writable(
|
||||
{
|
||||
pointerNearTop: false,
|
||||
pointerNearLeft: false,
|
||||
showIndicator: false,
|
||||
indicatorValue: null,
|
||||
})
|
||||
|
||||
const debounceDrag = debounce(() => { store.update(s => { s.showIndicator = false; return s }) }, 1000)
|
||||
|
||||
function showIndicator(pointerX: number, pointerY: number, value: any) {
|
||||
if (!window)
|
||||
return;
|
||||
|
||||
const state = get(store)
|
||||
|
||||
let middleWidth = window.innerWidth / 2;
|
||||
let middleHeight = window.innerHeight / 2;
|
||||
const pointerNearLeft = pointerX < middleWidth;
|
||||
const pointerNearTop = pointerY < middleHeight;
|
||||
store.update(s => { return { ...s, pointerNearTop, pointerNearLeft, showIndicator: true, indicatorValue: value } });
|
||||
debounceDrag();
|
||||
}
|
||||
|
||||
const interfaceStateStore: WritableInterfaceStateStore =
|
||||
{
|
||||
...store,
|
||||
showIndicator
|
||||
}
|
||||
export default interfaceStateStore;
|
||||
@@ -1,11 +1,9 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type ComfyApp from "$lib/components/ComfyApp"
|
||||
|
||||
export type UIEditMode = "widgets" | "containers" | "layout";
|
||||
|
||||
export type UIState = {
|
||||
app: ComfyApp,
|
||||
nodesLocked: boolean,
|
||||
graphLocked: boolean,
|
||||
autoAddUI: boolean,
|
||||
@@ -16,7 +14,6 @@ export type UIState = {
|
||||
export type WritableUIStateStore = Writable<UIState>;
|
||||
const store: WritableUIStateStore = writable(
|
||||
{
|
||||
app: null,
|
||||
graphLocked: false,
|
||||
nodesLocked: false,
|
||||
autoAddUI: true,
|
||||
|
||||
@@ -108,3 +108,13 @@ export function getNodeInfo(nodeId: number): string {
|
||||
const title = app.lGraph.getNodeById(nodeId)?.title || String(nodeId);
|
||||
return title + " (" + nodeId + ")"
|
||||
}
|
||||
|
||||
export const debounce = (callback: Function, wait = 250) => {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
return (...args: Array<unknown>) => {
|
||||
const next = () => callback(...args);
|
||||
if (timeout) clearTimeout(timeout);
|
||||
|
||||
timeout = setTimeout(next, wait);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Range } from "$lib/components/gradio/form";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { debounce } from "$lib/utils";
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfySliderNode | null = null;
|
||||
let nodeValue: Writable<number> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: number | null = null;
|
||||
let isDragging: boolean = false;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
@@ -19,6 +22,7 @@
|
||||
propsChanged = node.propsChanged;
|
||||
setOption($nodeValue); // don't react on option
|
||||
}
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
// I don't know why but this is necessary to watch for changes to node
|
||||
@@ -33,6 +37,23 @@
|
||||
option = value;
|
||||
}
|
||||
|
||||
function setBackgroundSize(input: HTMLInputElement) {
|
||||
input.style.setProperty("--background-size", `${getBackgroundSize(input)}%`);
|
||||
}
|
||||
|
||||
function getBackgroundSize(input: HTMLInputElement) {
|
||||
const min = +input.min || 0;
|
||||
const max = +input.max || 100;
|
||||
const value = +input.value;
|
||||
|
||||
return (value - min) / (max - min) * 100;
|
||||
}
|
||||
|
||||
function updateSliderForMobile() {
|
||||
const target = elem.querySelector<HTMLInputElement>("input[type=range]");
|
||||
setBackgroundSize(target);
|
||||
}
|
||||
|
||||
function onRelease(e: Event) {
|
||||
if (nodeValue && option) {
|
||||
$nodeValue = option
|
||||
@@ -42,6 +63,10 @@
|
||||
|
||||
let elem: HTMLDivElement = null;
|
||||
|
||||
$: if (elem) {
|
||||
updateSliderForMobile()
|
||||
}
|
||||
|
||||
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
|
||||
const slider = elem.querySelector("input[type='range']") as any
|
||||
//const range_selectors = "[id$='_clone']:is(input[type='range'])";
|
||||
@@ -51,6 +76,14 @@
|
||||
const style = elem.style;
|
||||
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gradio-slider" class:mobile={isMobile} bind:this={elem}>
|
||||
@@ -64,7 +97,9 @@
|
||||
label={widget.attrs.title}
|
||||
show_label={true}
|
||||
on:release={onRelease}
|
||||
on:change
|
||||
on:change={updateSliderForMobile}
|
||||
on:pointerdown={onPointerDown}
|
||||
on:pointermove={onPointerMove}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -77,9 +112,32 @@
|
||||
// Prevent swiping on the slider track from accidentally changing the value
|
||||
&.mobile :global(input[type="range"]) {
|
||||
pointer-events: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: default;
|
||||
height: 0.6rem;
|
||||
padding: initial;
|
||||
border: initial;
|
||||
margin: 0.8rem 0;
|
||||
width: 100%;
|
||||
|
||||
background: linear-gradient(to right, var(--color-blue-600), var(--color-blue-600)), #D7D7D7;
|
||||
background-size: var(--background-size, 0%) 100%;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--neutral-400);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
pointer-events: all;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-blue-600);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--neutral-100);
|
||||
box-shadow: 0px 0px 0px 1px var(--neutral-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,23 @@ import AppMobile from './AppMobile.svelte';
|
||||
import Framework7 from 'framework7/lite-bundle';
|
||||
import Framework7Svelte from 'framework7-svelte';
|
||||
import { f7 } from 'framework7-svelte';
|
||||
|
||||
import ComfyApp from '$lib/components/ComfyApp';
|
||||
import uiState from '$lib/stores/uiState';
|
||||
import { LiteGraph } from '@litegraph-ts/core';
|
||||
|
||||
Framework7.use(Framework7Svelte);
|
||||
|
||||
LiteGraph.dialog_close_on_mouse_leave = false;
|
||||
LiteGraph.search_hide_on_mouse_leave = false;
|
||||
LiteGraph.pointerevents_method = "pointer";
|
||||
|
||||
const comfyApp = new ComfyApp();
|
||||
|
||||
uiState.update(s => { s.app = comfyApp; return s; })
|
||||
|
||||
const app = new AppMobile({
|
||||
target: document.getElementById('app'),
|
||||
target: document.getElementById('app'),
|
||||
props: { app: comfyApp }
|
||||
})
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -7,14 +7,13 @@
|
||||
|
||||
import { Link, Toolbar } from "framework7-svelte"
|
||||
import ProgressBar from "$lib/components/ProgressBar.svelte";
|
||||
import Indicator from "./Indicator.svelte";
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
|
||||
export let subworkflowID: number = -1;
|
||||
let app: ComfyApp = undefined;
|
||||
export let app: ComfyApp = undefined;
|
||||
let fileInput: HTMLInputElement = undefined;
|
||||
|
||||
$: if (!app)
|
||||
app = $uiState.app
|
||||
|
||||
function queuePrompt() {
|
||||
app.queuePrompt(0, 1);
|
||||
notify("Prompt was queued", "Queued");
|
||||
@@ -56,6 +55,9 @@
|
||||
<Link on:click={doLoad}>Load</Link>
|
||||
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
|
||||
</Toolbar>
|
||||
{#if $interfaceState.showIndicator}
|
||||
<Indicator value={$interfaceState.indicatorValue} />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
#comfy-file-input {
|
||||
|
||||
45
src/mobile/Indicator.svelte
Normal file
45
src/mobile/Indicator.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
|
||||
export let value: any = null;
|
||||
</script>
|
||||
|
||||
<div style="position: relative; z-index: 10;">
|
||||
<div class="indicator"
|
||||
class:top={true}
|
||||
class:bottom={false}
|
||||
class:left={!$interfaceState.pointerNearLeft || !$interfaceState.pointerNearTop}
|
||||
class:right={$interfaceState.pointerNearLeft && $interfaceState.pointerNearTop}>
|
||||
<span>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.indicator {
|
||||
position: fixed;
|
||||
|
||||
align-content: left;
|
||||
padding: 1rem;
|
||||
font-size: xxx-large;
|
||||
background: var(--neutral-300);
|
||||
color: var(--neutral-800);
|
||||
border-radius: 1rem;
|
||||
border: 0.2rem solid var(--neutral-400);
|
||||
z-index: var(--layer-top) !important;
|
||||
|
||||
&.top {
|
||||
top: calc(1rem + var(--f7-navbar-height) + var(--f7-safe-area-top));
|
||||
}
|
||||
&.bottom {
|
||||
bottom: 5rem
|
||||
}
|
||||
&.left {
|
||||
left: 1rem;
|
||||
}
|
||||
&.right {
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,13 +6,10 @@
|
||||
import { onMount } from 'svelte';
|
||||
import uiState from "$lib/stores/uiState"
|
||||
|
||||
let app: ComfyApp | null = null;
|
||||
export let app: ComfyApp | null = null;
|
||||
let lCanvas: ComfyGraphCanvas | null = null;
|
||||
let canvasEl: HTMLCanvasElement | null = null;
|
||||
|
||||
$: if (!app)
|
||||
app = $uiState.app
|
||||
|
||||
function resizeCanvas() {
|
||||
canvasEl.width = canvasEl.parentElement.offsetWidth;
|
||||
canvasEl.height = canvasEl.parentElement.offsetHeight;
|
||||
@@ -21,13 +18,10 @@
|
||||
lCanvas.draw(true, true);
|
||||
}
|
||||
|
||||
$: if (app != null && canvasEl != null) {
|
||||
$: if (app != null && app.lGraph && canvasEl != null) {
|
||||
if (!lCanvas) {
|
||||
lCanvas = new ComfyGraphCanvas(app, canvasEl);
|
||||
lCanvas.allow_interaction = false;
|
||||
LiteGraph.dialog_close_on_mouse_leave = false;
|
||||
LiteGraph.search_hide_on_mouse_leave = false;
|
||||
LiteGraph.pointerevents_method = "pointer";
|
||||
app.lGraph.eventBus.on("afterExecute", () => lCanvas.draw(true))
|
||||
}
|
||||
resizeCanvas();
|
||||
@@ -47,5 +41,10 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #333;
|
||||
|
||||
> canvas {
|
||||
// Don't try to scroll the page when scrolling on canvas
|
||||
touch-action: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,22 +14,7 @@
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem } from "framework7-svelte"
|
||||
|
||||
let app: ComfyApp | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
if (app)
|
||||
return
|
||||
|
||||
app = $uiState.app = new ComfyApp();
|
||||
|
||||
app.api.addEventListener("status", (ev: CustomEvent) => {
|
||||
queueState.statusUpdated(ev.detail as ComfyAPIStatus);
|
||||
});
|
||||
|
||||
await app.setup();
|
||||
(window as any).app = app;
|
||||
|
||||
});
|
||||
export let app: ComfyApp | null = null;
|
||||
|
||||
</script>
|
||||
|
||||
@@ -52,8 +37,4 @@
|
||||
<i class="icon icon-f7" slot="media" />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<div class="canvas-wrapper pane-wrapper" style="display: none">
|
||||
<canvas id="graph-canvas" />
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||
import { Button } from "@gradio/button";
|
||||
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import { download } from "$lib/utils"
|
||||
|
||||
import { LGraph, LGraphNode } from "@litegraph-ts/core";
|
||||
import type { ComfyAPIStatus } from "$lib/api";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
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">
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
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";
|
||||
|
||||
export let subworkflowID: number = -1;
|
||||
export let app: ComfyApp
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user