Grid view for history
This commit is contained in:
@@ -73,6 +73,7 @@
|
||||
"klecks": "workspace:*",
|
||||
"pollen-css": "^4.6.2",
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"svelte-bootstrap-icons": "^2.3.1",
|
||||
"svelte-feather-icons": "^4.0.0",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"svelte-select": "^5.5.3",
|
||||
|
||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -97,6 +97,9 @@ importers:
|
||||
radix-icons-svelte:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
svelte-bootstrap-icons:
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
svelte-feather-icons:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@@ -7908,6 +7911,10 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
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):
|
||||
resolution: {integrity: sha512-oJux/afbmcZO+N+ADXB88h6XANLie8Y2rh2qBlhgfkpr2c3t/q/T0w2JWrHqagaDL8zeNwO8a8RVFBkrRox8gg==}
|
||||
hasBin: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ListIcon as List, ImageIcon as Image, SettingsIcon as Settings } from "svelte-feather-icons";
|
||||
import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp";
|
||||
import { Image, Gear } from "svelte-bootstrap-icons";
|
||||
import ComfyApp from "./ComfyApp";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import configState from "$lib/stores/configState";
|
||||
import workflowState from "$lib/stores/workflowState";
|
||||
@@ -62,7 +62,7 @@
|
||||
<SidebarItem id="generate" name="Generate" icon={Image}>
|
||||
<ComfyWorkflowsView {app} {uiTheme} />
|
||||
</SidebarItem>
|
||||
<SidebarItem id="settings" name="Settings" icon={Settings}>
|
||||
<SidebarItem id="settings" name="Settings" icon={Gear}>
|
||||
</SidebarItem>
|
||||
</Sidebar>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<label class="number-wrapper">
|
||||
<BlockTitle>{name}</BlockTitle>
|
||||
<div class="number">
|
||||
<input type="number" bind:value {min} {max} {step} {disabled}>
|
||||
<input type="number" bind:value {min} {max} {step} {disabled} />
|
||||
</div>
|
||||
</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">
|
||||
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
|
||||
import ProgressBar from "./ProgressBar.svelte";
|
||||
import Spinner from "./Spinner.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 type { Writable } from "svelte/store";
|
||||
import type { QueueItemType } from "$lib/api";
|
||||
@@ -14,6 +28,8 @@
|
||||
import Modal from "./Modal.svelte";
|
||||
import DropZone from "./DropZone.svelte";
|
||||
import workflowState from "$lib/stores/workflowState";
|
||||
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
|
||||
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
|
||||
|
||||
export let app: ComfyApp;
|
||||
|
||||
@@ -22,25 +38,16 @@
|
||||
let queueCompleted: Writable<CompletedQueueEntry[]> | 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) {
|
||||
queuePending = $queueState.queuePending
|
||||
queueRunning = $queueState.queueRunning
|
||||
queueCompleted = $queueState.queueCompleted
|
||||
}
|
||||
|
||||
type DisplayModeType = "list" | "grid";
|
||||
|
||||
let mode: QueueItemType = "queue";
|
||||
let displayMode: DisplayModeType = "list";
|
||||
let changed = true;
|
||||
|
||||
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 _runningEntries: QueueUIEntry[] = []
|
||||
let _entries: QueueUIEntry[] = []
|
||||
@@ -156,12 +173,12 @@
|
||||
console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted)
|
||||
}
|
||||
|
||||
function showLightbox(entry: QueueUIEntry, index: number, e: Event) {
|
||||
function showLightbox(images: string[], index: number, e: Event) {
|
||||
e.preventDefault()
|
||||
if (!entry.images)
|
||||
if (!images)
|
||||
return
|
||||
|
||||
ImageViewer.instance.showModal(entry.images, index);
|
||||
ImageViewer.instance.showModal(images, index);
|
||||
|
||||
e.stopPropagation()
|
||||
}
|
||||
@@ -184,7 +201,7 @@
|
||||
let expandAll = false;
|
||||
let selectedPrompt = null;
|
||||
let selectedImages = [];
|
||||
function showPrompt(entry: QueueUIEntry, e: MouseEvent) {
|
||||
function showPrompt(entry: QueueUIEntry) {
|
||||
selectedPrompt = entry.entry.prompt;
|
||||
selectedImages = entry.images;
|
||||
showModal = true;
|
||||
@@ -219,48 +236,33 @@
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div class="queue">
|
||||
<div class="queue-entries {mode}-mode" bind:this={queueList}>
|
||||
<div class="queue {mode}-mode">
|
||||
{#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}
|
||||
{#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>
|
||||
<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}
|
||||
{#if mode === "history" && displayMode === "grid"}
|
||||
<ComfyQueueGridDisplay entries={_entries} {showLightbox} {showPrompt} {mode} />
|
||||
{:else}
|
||||
<ComfyQueueListDisplay entries={_entries} {showLightbox} {showPrompt} {mode} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="queue-empty">
|
||||
<div class="queue-empty-container">
|
||||
<div class="queue-empty-icon">
|
||||
<List size="120rem" />
|
||||
<ListUl width="100%" height="10rem" />
|
||||
</div>
|
||||
<div class="queue-empty-message">
|
||||
(No entries)
|
||||
@@ -272,12 +274,12 @@
|
||||
<div class="mode-buttons">
|
||||
<div class="mode-button secondary"
|
||||
on:click={() => switchMode("queue")}
|
||||
class:mode-selected={mode === "queue"}>
|
||||
class:selected={mode === "queue"}>
|
||||
Queue
|
||||
</div>
|
||||
<div class="mode-button secondary"
|
||||
on:click={() => switchMode("history")}
|
||||
class:mode-selected={mode === "history"}>
|
||||
class:selected={mode === "history"}>
|
||||
History
|
||||
</div>
|
||||
</div>
|
||||
@@ -315,10 +317,12 @@
|
||||
|
||||
<style lang="scss">
|
||||
$pending-height: 200px;
|
||||
$display-mode-buttons-height: 2rem;
|
||||
$bottom-bar-height: 70px;
|
||||
$workflow-tabs-height: 2.5rem;
|
||||
$mode-buttons-height: 30px;
|
||||
$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 {
|
||||
padding-left: 0.2rem;
|
||||
@@ -329,20 +333,22 @@
|
||||
}
|
||||
.queue {
|
||||
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 {
|
||||
height: $queue-height;
|
||||
max-height: $queue-height;
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
&.queue-mode > :first-child {
|
||||
// elements stick to bottom in queue mode only
|
||||
// next element in queue is on the bottom
|
||||
margin-top: auto !important;
|
||||
}
|
||||
height: $queue-height;
|
||||
|
||||
> .queue-empty {
|
||||
display: flex;
|
||||
@@ -368,105 +374,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.queue-entry {
|
||||
padding: 1.0rem;
|
||||
.display-mode-buttons {
|
||||
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;
|
||||
top: 0px;
|
||||
height: $display-mode-buttons-height;
|
||||
margin-bottom: auto;
|
||||
|
||||
&:hover:not(:has(img:hover)) {
|
||||
cursor: pointer;
|
||||
background: var(--block-background-fill);
|
||||
> .mode-button {
|
||||
width: 100%;
|
||||
color: var(--neutral-500);
|
||||
|
||||
&.running {
|
||||
background: var(--comfy-accent-soft);
|
||||
&.selected {
|
||||
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 {
|
||||
@@ -502,13 +424,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.ternary {
|
||||
background: var(--panel-background-fill);
|
||||
&:hover {
|
||||
background: var(--block-background-fill );
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(85%);
|
||||
}
|
||||
&:active {
|
||||
filter: brightness(50%)
|
||||
}
|
||||
&.mode-selected {
|
||||
&.selected {
|
||||
filter: brightness(80%)
|
||||
}
|
||||
}
|
||||
@@ -521,7 +450,7 @@
|
||||
&:active {
|
||||
filter: brightness(50%)
|
||||
}
|
||||
&.mode-selected {
|
||||
&.selected {
|
||||
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">
|
||||
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 { BlockTitle } from "@gradio/atoms";
|
||||
import ComfyWorkflowView from "./ComfyWorkflowView.svelte";
|
||||
@@ -253,7 +253,7 @@
|
||||
</div>
|
||||
<button class="workflow-add-new-button"
|
||||
on:click={createNewWorkflow}>
|
||||
<PlusSquare size="100%" strokeWidth={1.5} />
|
||||
<PlusSquareDotted width="100%" height="100%" />
|
||||
</button>
|
||||
</div>
|
||||
<div id="bottombar">
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
{#if t.id === $selected_tab}
|
||||
<button class="selected">
|
||||
{#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}
|
||||
{t.name}
|
||||
{/if}
|
||||
@@ -75,7 +75,7 @@
|
||||
}}
|
||||
>
|
||||
{#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}
|
||||
{t.name}
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||
@@ -42,15 +42,17 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
||||
|
||||
selectedFilename: string | null = null;
|
||||
|
||||
selectedIndexWidget: ITextWidget;
|
||||
selectedIndexWidget: INumberWidget;
|
||||
modeWidget: IComboWidget;
|
||||
|
||||
imageWidth: Writable<number> = writable(0);
|
||||
imageHeight: Writable<number> = writable(0);
|
||||
|
||||
selectedImage: Writable<number | null> = writable(null);
|
||||
|
||||
constructor(name?: string) {
|
||||
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.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() {
|
||||
const value = get(this.value)
|
||||
const index = get(this.selectedImage)
|
||||
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) {
|
||||
const image = value[this.properties.index];
|
||||
this.selectedIndexWidget.value = index;
|
||||
|
||||
if (index != null && value && value[index] != null) {
|
||||
const image = value[index];
|
||||
image.width = get(this.imageWidth)
|
||||
image.height = get(this.imageHeight)
|
||||
}
|
||||
@@ -101,7 +106,6 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
||||
|
||||
override setValue(value: any, noChangedEvent: boolean = false) {
|
||||
super.setValue(value, noChangedEvent)
|
||||
this.setProperty("index", null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import type { Styles } from "@gradio/utils";
|
||||
import type { WidgetLayout } from "$lib/stores/layoutStates";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||
import { f7 } from "framework7-svelte";
|
||||
@@ -18,10 +17,9 @@
|
||||
let node: ComfyGalleryNode | null = null;
|
||||
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: number | null = null;
|
||||
let imageWidth: 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);
|
||||
|
||||
@@ -32,6 +30,7 @@
|
||||
propsChanged = node.propsChanged;
|
||||
imageWidth = node.imageWidth
|
||||
imageHeight = node.imageHeight
|
||||
selected_image = node.selectedImage;
|
||||
|
||||
if ($nodeValue != null) {
|
||||
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
|
||||
@@ -81,7 +80,7 @@
|
||||
thumbs: images.map(i => i.url),
|
||||
type: 'popup',
|
||||
});
|
||||
mobileLightbox.open(selected_image)
|
||||
mobileLightbox.open($selected_image)
|
||||
}
|
||||
|
||||
function onClicked(e: CustomEvent<HTMLImageElement>) {
|
||||
@@ -97,20 +96,6 @@
|
||||
// Update index
|
||||
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>
|
||||
|
||||
{#if widget && node && nodeValue && $nodeValue}
|
||||
@@ -148,7 +133,7 @@
|
||||
on:clicked={onClicked}
|
||||
bind:imageWidth={$imageWidth}
|
||||
bind:imageHeight={$imageHeight}
|
||||
bind:selected_image
|
||||
bind:selected_image={$selected_image}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
@@ -108,6 +108,12 @@ hr {
|
||||
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 {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user