Grid view for history
This commit is contained in:
@@ -73,6 +73,7 @@
|
|||||||
"klecks": "workspace:*",
|
"klecks": "workspace:*",
|
||||||
"pollen-css": "^4.6.2",
|
"pollen-css": "^4.6.2",
|
||||||
"radix-icons-svelte": "^1.2.1",
|
"radix-icons-svelte": "^1.2.1",
|
||||||
|
"svelte-bootstrap-icons": "^2.3.1",
|
||||||
"svelte-feather-icons": "^4.0.0",
|
"svelte-feather-icons": "^4.0.0",
|
||||||
"svelte-preprocess": "^5.0.3",
|
"svelte-preprocess": "^5.0.3",
|
||||||
"svelte-select": "^5.5.3",
|
"svelte-select": "^5.5.3",
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -97,6 +97,9 @@ importers:
|
|||||||
radix-icons-svelte:
|
radix-icons-svelte:
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
|
svelte-bootstrap-icons:
|
||||||
|
specifier: ^2.3.1
|
||||||
|
version: 2.3.1
|
||||||
svelte-feather-icons:
|
svelte-feather-icons:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
@@ -7908,6 +7911,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
/svelte-bootstrap-icons@2.3.1:
|
||||||
|
resolution: {integrity: sha512-Vqhgmcd55hEoB/MrPESvVYo4+m++2q9l00a5lzGkgbXTiO6go21PTU9DaeAJ446WBiEmg3Q8sv9QVOBmh5c1ww==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/svelte-check@2.2.6(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0):
|
/svelte-check@2.2.6(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0):
|
||||||
resolution: {integrity: sha512-oJux/afbmcZO+N+ADXB88h6XANLie8Y2rh2qBlhgfkpr2c3t/q/T0w2JWrHqagaDL8zeNwO8a8RVFBkrRox8gg==}
|
resolution: {integrity: sha512-oJux/afbmcZO+N+ADXB88h6XANLie8Y2rh2qBlhgfkpr2c3t/q/T0w2JWrHqagaDL8zeNwO8a8RVFBkrRox8gg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ListIcon as List, ImageIcon as Image, SettingsIcon as Settings } from "svelte-feather-icons";
|
import { Image, Gear } from "svelte-bootstrap-icons";
|
||||||
import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp";
|
import ComfyApp from "./ComfyApp";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import configState from "$lib/stores/configState";
|
import configState from "$lib/stores/configState";
|
||||||
import workflowState from "$lib/stores/workflowState";
|
import workflowState from "$lib/stores/workflowState";
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<SidebarItem id="generate" name="Generate" icon={Image}>
|
<SidebarItem id="generate" name="Generate" icon={Image}>
|
||||||
<ComfyWorkflowsView {app} {uiTheme} />
|
<ComfyWorkflowsView {app} {uiTheme} />
|
||||||
</SidebarItem>
|
</SidebarItem>
|
||||||
<SidebarItem id="settings" name="Settings" icon={Settings}>
|
<SidebarItem id="settings" name="Settings" icon={Gear}>
|
||||||
</SidebarItem>
|
</SidebarItem>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<label class="number-wrapper">
|
<label class="number-wrapper">
|
||||||
<BlockTitle>{name}</BlockTitle>
|
<BlockTitle>{name}</BlockTitle>
|
||||||
<div class="number">
|
<div class="number">
|
||||||
<input type="number" bind:value {min} {max} {step} {disabled}>
|
<input type="number" bind:value {min} {max} {step} {disabled} />
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
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
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
|
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
|
||||||
import ProgressBar from "./ProgressBar.svelte";
|
import ProgressBar from "./ProgressBar.svelte";
|
||||||
import Spinner from "./Spinner.svelte";
|
import Spinner from "./Spinner.svelte";
|
||||||
import PromptDisplay from "./PromptDisplay.svelte";
|
import PromptDisplay from "./PromptDisplay.svelte";
|
||||||
import { ListIcon as List } from "svelte-feather-icons";
|
import { List, ListUl, Grid } from "svelte-bootstrap-icons";
|
||||||
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo, truncateString } from "$lib/utils"
|
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo, truncateString } from "$lib/utils"
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { QueueItemType } from "$lib/api";
|
import type { QueueItemType } from "$lib/api";
|
||||||
@@ -14,6 +28,8 @@
|
|||||||
import Modal from "./Modal.svelte";
|
import Modal from "./Modal.svelte";
|
||||||
import DropZone from "./DropZone.svelte";
|
import DropZone from "./DropZone.svelte";
|
||||||
import workflowState from "$lib/stores/workflowState";
|
import workflowState from "$lib/stores/workflowState";
|
||||||
|
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
|
||||||
|
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
|
||||||
|
|
||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
|
|
||||||
@@ -22,25 +38,16 @@
|
|||||||
let queueCompleted: Writable<CompletedQueueEntry[]> | null = null;
|
let queueCompleted: Writable<CompletedQueueEntry[]> | null = null;
|
||||||
let queueList: HTMLDivElement | null = null;
|
let queueList: HTMLDivElement | null = null;
|
||||||
|
|
||||||
type QueueUIEntryStatus = QueueEntryStatus | "pending" | "running";
|
|
||||||
|
|
||||||
type QueueUIEntry = {
|
|
||||||
entry: QueueEntry,
|
|
||||||
message: string,
|
|
||||||
submessage: string,
|
|
||||||
date?: string,
|
|
||||||
status: QueueUIEntryStatus,
|
|
||||||
images?: string[], // URLs
|
|
||||||
details?: string // shown in a tooltip on hover
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if ($queueState) {
|
$: if ($queueState) {
|
||||||
queuePending = $queueState.queuePending
|
queuePending = $queueState.queuePending
|
||||||
queueRunning = $queueState.queueRunning
|
queueRunning = $queueState.queueRunning
|
||||||
queueCompleted = $queueState.queueCompleted
|
queueCompleted = $queueState.queueCompleted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DisplayModeType = "list" | "grid";
|
||||||
|
|
||||||
let mode: QueueItemType = "queue";
|
let mode: QueueItemType = "queue";
|
||||||
|
let displayMode: DisplayModeType = "list";
|
||||||
let changed = true;
|
let changed = true;
|
||||||
|
|
||||||
function switchMode(newMode: QueueItemType) {
|
function switchMode(newMode: QueueItemType) {
|
||||||
@@ -53,6 +60,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchDisplayMode(newDisplayMode: DisplayModeType) {
|
||||||
|
// changed = displayMode !== newDisplayMode
|
||||||
|
displayMode = newDisplayMode
|
||||||
|
// if (changed) {
|
||||||
|
// _queuedEntries = []
|
||||||
|
// _runningEntries = []
|
||||||
|
// _entries = []
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
let _queuedEntries: QueueUIEntry[] = []
|
let _queuedEntries: QueueUIEntry[] = []
|
||||||
let _runningEntries: QueueUIEntry[] = []
|
let _runningEntries: QueueUIEntry[] = []
|
||||||
let _entries: QueueUIEntry[] = []
|
let _entries: QueueUIEntry[] = []
|
||||||
@@ -156,12 +173,12 @@
|
|||||||
console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted)
|
console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLightbox(entry: QueueUIEntry, index: number, e: Event) {
|
function showLightbox(images: string[], index: number, e: Event) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!entry.images)
|
if (!images)
|
||||||
return
|
return
|
||||||
|
|
||||||
ImageViewer.instance.showModal(entry.images, index);
|
ImageViewer.instance.showModal(images, index);
|
||||||
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
@@ -184,7 +201,7 @@
|
|||||||
let expandAll = false;
|
let expandAll = false;
|
||||||
let selectedPrompt = null;
|
let selectedPrompt = null;
|
||||||
let selectedImages = [];
|
let selectedImages = [];
|
||||||
function showPrompt(entry: QueueUIEntry, e: MouseEvent) {
|
function showPrompt(entry: QueueUIEntry) {
|
||||||
selectedPrompt = entry.entry.prompt;
|
selectedPrompt = entry.entry.prompt;
|
||||||
selectedImages = entry.images;
|
selectedImages = entry.images;
|
||||||
showModal = true;
|
showModal = true;
|
||||||
@@ -219,48 +236,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div class="queue">
|
<div class="queue {mode}-mode">
|
||||||
<div class="queue-entries {mode}-mode" bind:this={queueList}>
|
{#if mode === "history"}
|
||||||
|
<div class="display-mode-buttons">
|
||||||
|
<div class="mode-button image-display-button ternary"
|
||||||
|
on:click={() => switchDisplayMode("list")}
|
||||||
|
class:selected={displayMode === "list"}>
|
||||||
|
<List width="100%" height="100%" />
|
||||||
|
</div>
|
||||||
|
<div class="mode-button image-display-button ternary"
|
||||||
|
on:click={() => switchDisplayMode("grid")}
|
||||||
|
class:selected={displayMode === "grid"}>
|
||||||
|
<Grid width="100%" height="100%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="queue-entries" bind:this={queueList}>
|
||||||
{#if _entries.length > 0}
|
{#if _entries.length > 0}
|
||||||
{#each _entries as entry}
|
{#if mode === "history" && displayMode === "grid"}
|
||||||
<div class="queue-entry {entry.status}" on:click={(e) => showPrompt(entry, e)}>
|
<ComfyQueueGridDisplay entries={_entries} {showLightbox} {showPrompt} {mode} />
|
||||||
{#if entry.images.length > 0}
|
{:else}
|
||||||
<div class="queue-entry-images"
|
<ComfyQueueListDisplay entries={_entries} {showLightbox} {showPrompt} {mode} />
|
||||||
style="--cols: {Math.ceil(Math.sqrt(Math.min(entry.images.length, 4)))}" >
|
{/if}
|
||||||
{#each entry.images.slice(0, 4) as image, i}
|
|
||||||
<div>
|
|
||||||
<img class="queue-entry-image"
|
|
||||||
on:click={(e) => showLightbox(entry, i, e)}
|
|
||||||
src={image}
|
|
||||||
alt="thumbnail" />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<!-- <div class="queue-entry-image-placeholder" /> -->
|
|
||||||
{/if}
|
|
||||||
<div class="queue-entry-details">
|
|
||||||
<div class="queue-entry-message">
|
|
||||||
{truncateString(entry.message, 20)}
|
|
||||||
</div>
|
|
||||||
<div class="queue-entry-submessage">
|
|
||||||
{entry.submessage}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="queue-entry-rest {entry.status}">
|
|
||||||
{#if entry.date != null}
|
|
||||||
<span class="queue-entry-queued-at">
|
|
||||||
{entry.date}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="queue-empty">
|
<div class="queue-empty">
|
||||||
<div class="queue-empty-container">
|
<div class="queue-empty-container">
|
||||||
<div class="queue-empty-icon">
|
<div class="queue-empty-icon">
|
||||||
<List size="120rem" />
|
<ListUl width="100%" height="10rem" />
|
||||||
</div>
|
</div>
|
||||||
<div class="queue-empty-message">
|
<div class="queue-empty-message">
|
||||||
(No entries)
|
(No entries)
|
||||||
@@ -272,12 +274,12 @@
|
|||||||
<div class="mode-buttons">
|
<div class="mode-buttons">
|
||||||
<div class="mode-button secondary"
|
<div class="mode-button secondary"
|
||||||
on:click={() => switchMode("queue")}
|
on:click={() => switchMode("queue")}
|
||||||
class:mode-selected={mode === "queue"}>
|
class:selected={mode === "queue"}>
|
||||||
Queue
|
Queue
|
||||||
</div>
|
</div>
|
||||||
<div class="mode-button secondary"
|
<div class="mode-button secondary"
|
||||||
on:click={() => switchMode("history")}
|
on:click={() => switchMode("history")}
|
||||||
class:mode-selected={mode === "history"}>
|
class:selected={mode === "history"}>
|
||||||
History
|
History
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -315,10 +317,12 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
$pending-height: 200px;
|
$pending-height: 200px;
|
||||||
|
$display-mode-buttons-height: 2rem;
|
||||||
$bottom-bar-height: 70px;
|
$bottom-bar-height: 70px;
|
||||||
$workflow-tabs-height: 2.5rem;
|
$workflow-tabs-height: 2.5rem;
|
||||||
$mode-buttons-height: 30px;
|
$mode-buttons-height: 30px;
|
||||||
$queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height} - #{$workflow-tabs-height} - 0.9rem);
|
$queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height} - #{$workflow-tabs-height} - 0.9rem);
|
||||||
|
$queue-height-history: calc(#{$queue-height} - #{$display-mode-buttons-height});
|
||||||
|
|
||||||
.prompt-modal-header {
|
.prompt-modal-header {
|
||||||
padding-left: 0.2rem;
|
padding-left: 0.2rem;
|
||||||
@@ -329,20 +333,22 @@
|
|||||||
}
|
}
|
||||||
.queue {
|
.queue {
|
||||||
color: var(--body-text-color);
|
color: var(--body-text-color);
|
||||||
|
|
||||||
|
&.queue-mode > .queue-entries {
|
||||||
|
height: $queue-height;
|
||||||
|
max-height: $queue-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.history-mode > .queue-entries {
|
||||||
|
height: $queue-height-history;
|
||||||
|
max-height: $queue-height-history;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-entries {
|
.queue-entries {
|
||||||
height: $queue-height;
|
|
||||||
max-height: $queue-height;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-y: auto;
|
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
|
height: $queue-height;
|
||||||
&.queue-mode > :first-child {
|
|
||||||
// elements stick to bottom in queue mode only
|
|
||||||
// next element in queue is on the bottom
|
|
||||||
margin-top: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .queue-empty {
|
> .queue-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -368,105 +374,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-entry {
|
.display-mode-buttons {
|
||||||
padding: 1.0rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
border-bottom: 1px solid var(--block-border-color);
|
top: 0px;
|
||||||
border-top: 1px solid var(--table-border-color);
|
height: $display-mode-buttons-height;
|
||||||
background: var(--panel-background-fill);
|
margin-bottom: auto;
|
||||||
max-height: 14rem;
|
|
||||||
|
|
||||||
&:hover:not(:has(img:hover)) {
|
> .mode-button {
|
||||||
cursor: pointer;
|
width: 100%;
|
||||||
background: var(--block-background-fill);
|
color: var(--neutral-500);
|
||||||
|
|
||||||
&.running {
|
&.selected {
|
||||||
background: var(--comfy-accent-soft);
|
color: var(--body-text-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
|
||||||
/* background: green; */
|
|
||||||
}
|
|
||||||
&.error {
|
|
||||||
background: red;
|
|
||||||
}
|
|
||||||
&.all_cached, &.interrupted {
|
|
||||||
filter: brightness(80%);
|
|
||||||
background: var(--comfy-disabled-textbox-background-fill);
|
|
||||||
color: var(--comfy-disable-textbox-text-color);
|
|
||||||
}
|
|
||||||
&.running {
|
|
||||||
background: var(--block-background-fill);
|
|
||||||
border: 3px dashed var(--neutral-500);
|
|
||||||
}
|
|
||||||
&.pending, &.unknown {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-entry-rest {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.all_cached, &.interrupted {
|
|
||||||
filter: brightness(80%);
|
|
||||||
color: var(--comfy-accent-soft);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$thumbnails-size: 12rem;
|
|
||||||
|
|
||||||
.queue-entry-images {
|
|
||||||
--cols: 1;
|
|
||||||
margin: auto;
|
|
||||||
width: calc($thumbnails-size * 2);
|
|
||||||
display: grid;
|
|
||||||
display: inline-grid;
|
|
||||||
grid-template-columns: repeat(var(--cols), 1fr);
|
|
||||||
grid-template-rows: repeat(var(--cols), 1fr);
|
|
||||||
column-gap: 1px;
|
|
||||||
row-gap: 1px;
|
|
||||||
vertical-align: top;
|
|
||||||
flex: 1 1 40%;
|
|
||||||
|
|
||||||
img {
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
object-fit: cover;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
filter: brightness(120%) contrast(120%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-entry-details {
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-entry-message {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-entry-submessage {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-entry-queued-at {
|
|
||||||
width: auto;
|
|
||||||
font-size: 12px;
|
|
||||||
position:absolute;
|
|
||||||
right: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
padding: 0.4rem 0.6rem;
|
|
||||||
color: var(--body-text-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-buttons {
|
.mode-buttons {
|
||||||
@@ -502,13 +424,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.ternary {
|
||||||
|
background: var(--panel-background-fill);
|
||||||
|
&:hover {
|
||||||
|
background: var(--block-background-fill );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(85%);
|
filter: brightness(85%);
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
filter: brightness(50%)
|
filter: brightness(50%)
|
||||||
}
|
}
|
||||||
&.mode-selected {
|
&.selected {
|
||||||
filter: brightness(80%)
|
filter: brightness(80%)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,7 +450,7 @@
|
|||||||
&:active {
|
&:active {
|
||||||
filter: brightness(50%)
|
filter: brightness(50%)
|
||||||
}
|
}
|
||||||
&.mode-selected {
|
&.selected {
|
||||||
filter: brightness(150%)
|
filter: brightness(150%)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/lib/components/ComfyQueueGridDisplay.svelte
Normal file
118
src/lib/components/ComfyQueueGridDisplay.svelte
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { QueueItemType } from "$lib/api";
|
||||||
|
import { truncateString } from "$lib/utils";
|
||||||
|
import type { QueueUIEntry } from "./ComfyQueue.svelte";
|
||||||
|
|
||||||
|
export let entries: QueueUIEntry[] = [];
|
||||||
|
export let showPrompt: (entry: QueueUIEntry) => void;
|
||||||
|
export let showLightbox: (images: string[], index: number, e: Event) => void;
|
||||||
|
export let mode: QueueItemType = "queue";
|
||||||
|
|
||||||
|
let allEntries: [QueueUIEntry, string][] = []
|
||||||
|
let allImages: string[] = []
|
||||||
|
let gridColumns: number = 3;
|
||||||
|
|
||||||
|
$: buildImageList(entries);
|
||||||
|
|
||||||
|
function buildImageList(entries: QueueUIEntry[]) {
|
||||||
|
allEntries = []
|
||||||
|
for (const entry of entries) {
|
||||||
|
for (const image of entry.images) {
|
||||||
|
allEntries.push([entry, image]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allImages = allEntries.map(p => p[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(e: MouseEvent, entry: QueueUIEntry, index: number) {
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
showPrompt(entry);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
showLightbox(allImages, index, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid-wrapper">
|
||||||
|
<div class="grid-columns">
|
||||||
|
<div>
|
||||||
|
<input type="range" bind:value={gridColumns} min={1} max={8} step={1} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-entries">
|
||||||
|
<div class="grid" style:--cols={gridColumns}>
|
||||||
|
{#each allEntries as [entry, image], i}
|
||||||
|
<div class="grid-entry">
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<img class="grid-entry-image"
|
||||||
|
on:click={(e) => handleClick(e, entry, i)}
|
||||||
|
src={image}
|
||||||
|
alt="thumbnail" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$grid-columns-control-height: 3rem;
|
||||||
|
|
||||||
|
.grid-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-columns {
|
||||||
|
width: 100%;
|
||||||
|
height: $grid-columns-control-height;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--spacing-lg) 0;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-entries {
|
||||||
|
height: calc(100% - #{$grid-columns-control-height});
|
||||||
|
padding: 0 var(--spacing-lg) var(--spacing-lg) var(--spacing-lg);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
--cols: 3;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
filter: brightness(120%) contrast(120%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
162
src/lib/components/ComfyQueueListDisplay.svelte
Normal file
162
src/lib/components/ComfyQueueListDisplay.svelte
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { QueueItemType } from "$lib/api";
|
||||||
|
import { truncateString } from "$lib/utils";
|
||||||
|
import type { QueueUIEntry } from "./ComfyQueue.svelte";
|
||||||
|
|
||||||
|
export let entries: QueueUIEntry[] = [];
|
||||||
|
export let showPrompt: (entry: QueueUIEntry) => void;
|
||||||
|
export let showLightbox: (images: string[], index: number, e: Event) => void;
|
||||||
|
export let mode: QueueItemType = "queue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="queue-wrapper {mode}-mode">
|
||||||
|
{#each entries as entry}
|
||||||
|
<div class="queue-entry {entry.status}" on:click={(e) => showPrompt(entry, e)}>
|
||||||
|
{#if entry.images.length > 0}
|
||||||
|
<div class="queue-entry-images"
|
||||||
|
style="--cols: {Math.ceil(Math.sqrt(Math.min(entry.images.length, 4)))}" >
|
||||||
|
{#each entry.images.slice(0, 4) as image, i}
|
||||||
|
<div>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<img class="queue-entry-image"
|
||||||
|
on:click={(e) => showLightbox(entry.images, i, e)}
|
||||||
|
src={image}
|
||||||
|
alt="thumbnail" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="queue-entry-details">
|
||||||
|
<div class="queue-entry-message">
|
||||||
|
{truncateString(entry.message, 20)}
|
||||||
|
</div>
|
||||||
|
<div class="queue-entry-submessage">
|
||||||
|
{entry.submessage}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="queue-entry-rest {entry.status}">
|
||||||
|
{#if entry.date != null}
|
||||||
|
<span class="queue-entry-queued-at">
|
||||||
|
{entry.date}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.queue-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
|
||||||
|
&.queue-mode > :global(:first-child) {
|
||||||
|
// elements stick to bottom in queue mode only
|
||||||
|
// next element in queue is on the bottom
|
||||||
|
margin-top: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-entry {
|
||||||
|
padding: 1.0rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
border-bottom: 1px solid var(--block-border-color);
|
||||||
|
border-top: 1px solid var(--table-border-color);
|
||||||
|
background: var(--panel-background-fill);
|
||||||
|
max-height: 14rem;
|
||||||
|
|
||||||
|
&:hover:not(:has(img:hover)) {
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--block-background-fill);
|
||||||
|
|
||||||
|
&.running {
|
||||||
|
background: var(--comfy-accent-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
/* background: green; */
|
||||||
|
}
|
||||||
|
&.error {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
&.all_cached, &.interrupted {
|
||||||
|
filter: brightness(80%);
|
||||||
|
background: var(--comfy-disabled-textbox-background-fill);
|
||||||
|
color: var(--comfy-disable-textbox-text-color);
|
||||||
|
}
|
||||||
|
&.running {
|
||||||
|
background: var(--block-background-fill);
|
||||||
|
border: 3px dashed var(--neutral-500);
|
||||||
|
}
|
||||||
|
&.pending, &.unknown {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-entry-rest {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.all_cached, &.interrupted {
|
||||||
|
filter: brightness(80%);
|
||||||
|
color: var(--comfy-accent-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$thumbnails-size: 12rem;
|
||||||
|
|
||||||
|
.queue-entry-images {
|
||||||
|
--cols: 1;
|
||||||
|
margin: auto;
|
||||||
|
width: calc($thumbnails-size * 2);
|
||||||
|
display: grid;
|
||||||
|
display: inline-grid;
|
||||||
|
grid-template-columns: repeat(var(--cols), 1fr);
|
||||||
|
grid-template-rows: repeat(var(--cols), 1fr);
|
||||||
|
column-gap: 1px;
|
||||||
|
row-gap: 1px;
|
||||||
|
vertical-align: top;
|
||||||
|
flex: 1 1 40%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
filter: brightness(120%) contrast(120%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-entry-details {
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-entry-message {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-entry-submessage {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-entry-queued-at {
|
||||||
|
width: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
position:absolute;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
color: var(--body-text-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||||
import { PlusSquareIcon as PlusSquare } from 'svelte-feather-icons';
|
import { PlusSquareDotted } from 'svelte-bootstrap-icons';
|
||||||
import { Button } from "@gradio/button";
|
import { Button } from "@gradio/button";
|
||||||
import { BlockTitle } from "@gradio/atoms";
|
import { BlockTitle } from "@gradio/atoms";
|
||||||
import ComfyWorkflowView from "./ComfyWorkflowView.svelte";
|
import ComfyWorkflowView from "./ComfyWorkflowView.svelte";
|
||||||
@@ -253,7 +253,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="workflow-add-new-button"
|
<button class="workflow-add-new-button"
|
||||||
on:click={createNewWorkflow}>
|
on:click={createNewWorkflow}>
|
||||||
<PlusSquare size="100%" strokeWidth={1.5} />
|
<PlusSquareDotted width="100%" height="100%" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="bottombar">
|
<div id="bottombar">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
{#if t.id === $selected_tab}
|
{#if t.id === $selected_tab}
|
||||||
<button class="selected">
|
<button class="selected">
|
||||||
{#if t.icon !== null}
|
{#if t.icon !== null}
|
||||||
<svelte:component this={t.icon} size="100%" strokeWidth={1.5} />
|
<svelte:component this={t.icon} width="100%" height="100%" strokeWidth={1.5} />
|
||||||
{:else}
|
{:else}
|
||||||
{t.name}
|
{t.name}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if t.icon !== null}
|
{#if t.icon !== null}
|
||||||
<svelte:component this={t.icon} size="100%" strokeWidth={1.5} />
|
<svelte:component this={t.icon} width="100%" height="100%" strokeWidth={1.5} />
|
||||||
{:else}
|
{:else}
|
||||||
{t.name}
|
{t.name}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata, type ComfyUploadImageType } from "$lib/utils";
|
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata, type ComfyUploadImageType } from "$lib/utils";
|
||||||
import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout, type INumberWidget } from "@litegraph-ts/core";
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||||
@@ -42,15 +42,17 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
|||||||
|
|
||||||
selectedFilename: string | null = null;
|
selectedFilename: string | null = null;
|
||||||
|
|
||||||
selectedIndexWidget: ITextWidget;
|
selectedIndexWidget: INumberWidget;
|
||||||
modeWidget: IComboWidget;
|
modeWidget: IComboWidget;
|
||||||
|
|
||||||
imageWidth: Writable<number> = writable(0);
|
imageWidth: Writable<number> = writable(0);
|
||||||
imageHeight: Writable<number> = writable(0);
|
imageHeight: Writable<number> = writable(0);
|
||||||
|
|
||||||
|
selectedImage: Writable<number | null> = writable(null);
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
super(name, [])
|
super(name, [])
|
||||||
this.selectedIndexWidget = this.addWidget("text", "Selected", String(this.properties.index), "index")
|
this.selectedIndexWidget = this.addWidget("number", "Selected", get(this.selectedImage))
|
||||||
this.selectedIndexWidget.disabled = true;
|
this.selectedIndexWidget.disabled = true;
|
||||||
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
|
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
|
||||||
}
|
}
|
||||||
@@ -63,11 +65,14 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
|||||||
|
|
||||||
override onExecute() {
|
override onExecute() {
|
||||||
const value = get(this.value)
|
const value = get(this.value)
|
||||||
|
const index = get(this.selectedImage)
|
||||||
this.setOutputData(0, value)
|
this.setOutputData(0, value)
|
||||||
this.setOutputData(1, this.properties.index)
|
this.setOutputData(1, index)
|
||||||
|
|
||||||
if (this.properties.index != null && value && value[this.properties.index] != null) {
|
this.selectedIndexWidget.value = index;
|
||||||
const image = value[this.properties.index];
|
|
||||||
|
if (index != null && value && value[index] != null) {
|
||||||
|
const image = value[index];
|
||||||
image.width = get(this.imageWidth)
|
image.width = get(this.imageWidth)
|
||||||
image.height = get(this.imageHeight)
|
image.height = get(this.imageHeight)
|
||||||
}
|
}
|
||||||
@@ -101,7 +106,6 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
|||||||
|
|
||||||
override setValue(value: any, noChangedEvent: boolean = false) {
|
override setValue(value: any, noChangedEvent: boolean = false) {
|
||||||
super.setValue(value, noChangedEvent)
|
super.setValue(value, noChangedEvent)
|
||||||
this.setProperty("index", null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
import type { Styles } from "@gradio/utils";
|
import type { Styles } from "@gradio/utils";
|
||||||
import type { WidgetLayout } from "$lib/stores/layoutStates";
|
import type { WidgetLayout } from "$lib/stores/layoutStates";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
|
||||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||||
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";
|
||||||
@@ -18,10 +17,9 @@
|
|||||||
let node: ComfyGalleryNode | null = null;
|
let node: ComfyGalleryNode | null = null;
|
||||||
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||||
let propsChanged: Writable<number> | null = null;
|
let propsChanged: Writable<number> | null = null;
|
||||||
let option: number | null = null;
|
|
||||||
let imageWidth: Writable<number> = writable(0);
|
let imageWidth: Writable<number> = writable(0);
|
||||||
let imageHeight: Writable<number> = writable(0);
|
let imageHeight: Writable<number> = writable(0);
|
||||||
let selected_image: number | null = null;
|
let selected_image: Writable<number | null> = writable(null);
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
@@ -32,6 +30,7 @@
|
|||||||
propsChanged = node.propsChanged;
|
propsChanged = node.propsChanged;
|
||||||
imageWidth = node.imageWidth
|
imageWidth = node.imageWidth
|
||||||
imageHeight = node.imageHeight
|
imageHeight = node.imageHeight
|
||||||
|
selected_image = node.selectedImage;
|
||||||
|
|
||||||
if ($nodeValue != null) {
|
if ($nodeValue != null) {
|
||||||
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
|
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
|
||||||
@@ -81,7 +80,7 @@
|
|||||||
thumbs: images.map(i => i.url),
|
thumbs: images.map(i => i.url),
|
||||||
type: 'popup',
|
type: 'popup',
|
||||||
});
|
});
|
||||||
mobileLightbox.open(selected_image)
|
mobileLightbox.open($selected_image)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClicked(e: CustomEvent<HTMLImageElement>) {
|
function onClicked(e: CustomEvent<HTMLImageElement>) {
|
||||||
@@ -97,20 +96,6 @@
|
|||||||
// Update index
|
// Update index
|
||||||
node.setProperty("index", e.detail.index as number)
|
node.setProperty("index", e.detail.index as number)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($propsChanged > -1 && widget && $nodeValue) {
|
|
||||||
if (widget.attrs.variant === "image") {
|
|
||||||
node.setProperty("index", selected_image)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
node.setProperty("index", $nodeValue.length > 0 ? 0 : null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
node.setProperty("index", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: node.setProperty("index", selected_image)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if widget && node && nodeValue && $nodeValue}
|
{#if widget && node && nodeValue && $nodeValue}
|
||||||
@@ -148,7 +133,7 @@
|
|||||||
on:clicked={onClicked}
|
on:clicked={onClicked}
|
||||||
bind:imageWidth={$imageWidth}
|
bind:imageWidth={$imageWidth}
|
||||||
bind:imageHeight={$imageHeight}
|
bind:imageHeight={$imageHeight}
|
||||||
bind:selected_image
|
bind:selected_image={$selected_image}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ hr {
|
|||||||
color: var(--panel-border-color);
|
color: var(--panel-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
color: var(--body-text-color);
|
||||||
|
background: var(--input-background-fill);
|
||||||
|
border: var(--input-border-width) solid var(--input-border-color)
|
||||||
|
}
|
||||||
|
|
||||||
input:not(input[type=radio]), textarea {
|
input:not(input[type=radio]), textarea {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user