@gradio/gallery 0.12.1 → 0.13.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,624 @@
1
+ <script>import { BlockLabel, Empty, ShareButton } from "@gradio/atoms";
2
+ import { ModifyUpload } from "@gradio/upload";
3
+ import { Image } from "@gradio/image/shared";
4
+ import { Video } from "@gradio/video/shared";
5
+ import { dequal } from "dequal";
6
+ import { createEventDispatcher, onMount } from "svelte";
7
+ import { tick } from "svelte";
8
+ import {
9
+ Download,
10
+ Image as ImageIcon,
11
+ Maximize,
12
+ Minimize,
13
+ Clear,
14
+ Play
15
+ } from "@gradio/icons";
16
+ import { FileData } from "@gradio/client";
17
+ import { format_gallery_for_sharing } from "./utils";
18
+ import { IconButton } from "@gradio/atoms";
19
+ export let show_label = true;
20
+ export let label;
21
+ export let value = null;
22
+ export let columns = [2];
23
+ export let rows = void 0;
24
+ export let height = "auto";
25
+ export let preview;
26
+ export let allow_preview = true;
27
+ export let object_fit = "cover";
28
+ export let show_share_button = false;
29
+ export let show_download_button = false;
30
+ export let i18n;
31
+ export let selected_index = null;
32
+ export let interactive;
33
+ export let _fetch;
34
+ export let mode = "normal";
35
+ export let show_fullscreen_button = true;
36
+ let is_full_screen = false;
37
+ let gallery_container;
38
+ const dispatch = createEventDispatcher();
39
+ let was_reset = true;
40
+ $:
41
+ was_reset = value == null || value.length === 0 ? true : was_reset;
42
+ let resolved_value = null;
43
+ $:
44
+ resolved_value = value == null ? null : value.map((data) => {
45
+ if ("video" in data) {
46
+ return {
47
+ video: data.video,
48
+ caption: data.caption
49
+ };
50
+ } else if ("image" in data) {
51
+ return { image: data.image, caption: data.caption };
52
+ }
53
+ return {};
54
+ });
55
+ let prev_value = value;
56
+ if (selected_index == null && preview && value?.length) {
57
+ selected_index = 0;
58
+ }
59
+ let old_selected_index = selected_index;
60
+ $:
61
+ if (!dequal(prev_value, value)) {
62
+ if (was_reset) {
63
+ selected_index = preview && value?.length ? 0 : null;
64
+ was_reset = false;
65
+ } else {
66
+ selected_index = selected_index != null && value != null && selected_index < value.length ? selected_index : null;
67
+ }
68
+ dispatch("change");
69
+ prev_value = value;
70
+ }
71
+ $:
72
+ previous = ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) % (resolved_value?.length ?? 0);
73
+ $:
74
+ next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
75
+ function handle_preview_click(event) {
76
+ const element = event.target;
77
+ const x = event.offsetX;
78
+ const width = element.offsetWidth;
79
+ const centerX = width / 2;
80
+ if (x < centerX) {
81
+ selected_index = previous;
82
+ } else {
83
+ selected_index = next;
84
+ }
85
+ }
86
+ function on_keydown(e) {
87
+ switch (e.code) {
88
+ case "Escape":
89
+ e.preventDefault();
90
+ selected_index = null;
91
+ break;
92
+ case "ArrowLeft":
93
+ e.preventDefault();
94
+ selected_index = previous;
95
+ break;
96
+ case "ArrowRight":
97
+ e.preventDefault();
98
+ selected_index = next;
99
+ break;
100
+ default:
101
+ break;
102
+ }
103
+ }
104
+ $: {
105
+ if (selected_index !== old_selected_index) {
106
+ old_selected_index = selected_index;
107
+ if (selected_index !== null) {
108
+ dispatch("select", {
109
+ index: selected_index,
110
+ value: resolved_value?.[selected_index]
111
+ });
112
+ }
113
+ }
114
+ }
115
+ $:
116
+ if (allow_preview) {
117
+ scroll_to_img(selected_index);
118
+ }
119
+ let el = [];
120
+ let container_element;
121
+ async function scroll_to_img(index) {
122
+ if (typeof index !== "number")
123
+ return;
124
+ await tick();
125
+ if (el[index] === void 0)
126
+ return;
127
+ el[index]?.focus();
128
+ const { left: container_left, width: container_width } = container_element.getBoundingClientRect();
129
+ const { left, width } = el[index].getBoundingClientRect();
130
+ const relative_left = left - container_left;
131
+ const pos = relative_left + width / 2 - container_width / 2 + container_element.scrollLeft;
132
+ if (container_element && typeof container_element.scrollTo === "function") {
133
+ container_element.scrollTo({
134
+ left: pos < 0 ? 0 : pos,
135
+ behavior: "smooth"
136
+ });
137
+ }
138
+ }
139
+ let window_height = 0;
140
+ async function download(file_url, name) {
141
+ let response;
142
+ try {
143
+ response = await _fetch(file_url);
144
+ } catch (error) {
145
+ if (error instanceof TypeError) {
146
+ window.open(file_url, "_blank", "noreferrer");
147
+ return;
148
+ }
149
+ throw error;
150
+ }
151
+ const blob = await response.blob();
152
+ const url = URL.createObjectURL(blob);
153
+ const link = document.createElement("a");
154
+ link.href = url;
155
+ link.download = name;
156
+ link.click();
157
+ URL.revokeObjectURL(url);
158
+ }
159
+ $:
160
+ selected_media = selected_index != null && resolved_value != null ? resolved_value[selected_index] : null;
161
+ onMount(() => {
162
+ document.addEventListener("fullscreenchange", () => {
163
+ is_full_screen = !!document.fullscreenElement;
164
+ });
165
+ });
166
+ const toggle_full_screen = async () => {
167
+ if (!is_full_screen) {
168
+ await gallery_container.requestFullscreen();
169
+ } else {
170
+ await document.exitFullscreen();
171
+ }
172
+ };
173
+ </script>
174
+
175
+ <svelte:window bind:innerHeight={window_height} />
176
+
177
+ {#if show_label}
178
+ <BlockLabel {show_label} Icon={ImageIcon} label={label || "Gallery"} />
179
+ {/if}
180
+ {#if value == null || resolved_value == null || resolved_value.length === 0}
181
+ <Empty unpadded_box={true} size="large"><ImageIcon /></Empty>
182
+ {:else}
183
+ <div class="gallery-container" bind:this={gallery_container}>
184
+ {#if selected_media && allow_preview}
185
+ <button
186
+ on:keydown={on_keydown}
187
+ class="preview"
188
+ class:minimal={mode === "minimal"}
189
+ >
190
+ <div class="icon-buttons">
191
+ {#if show_download_button}
192
+ <IconButton
193
+ Icon={Download}
194
+ label={i18n("common.download")}
195
+ on:click={() => {
196
+ const image =
197
+ "image" in selected_media
198
+ ? selected_media?.image
199
+ : selected_media?.video;
200
+ if (image == null) {
201
+ return;
202
+ }
203
+ const { url, orig_name } = image;
204
+ if (url) {
205
+ download(url, orig_name ?? "image");
206
+ }
207
+ }}
208
+ />
209
+ {/if}
210
+
211
+ {#if show_fullscreen_button && !is_full_screen}
212
+ <IconButton
213
+ Icon={is_full_screen ? Minimize : Maximize}
214
+ label={is_full_screen
215
+ ? "Exit full screen"
216
+ : "View in full screen"}
217
+ on:click={toggle_full_screen}
218
+ />
219
+ {/if}
220
+
221
+ {#if show_fullscreen_button && is_full_screen}
222
+ <IconButton
223
+ Icon={Minimize}
224
+ label="Exit full screen"
225
+ on:click={toggle_full_screen}
226
+ />
227
+ {/if}
228
+
229
+ {#if !is_full_screen}
230
+ <IconButton
231
+ Icon={Clear}
232
+ label="Close"
233
+ on:click={() => (selected_index = null)}
234
+ />
235
+ {/if}
236
+ </div>
237
+ <button
238
+ class="media-button"
239
+ on:click={"image" in selected_media
240
+ ? (event) => handle_preview_click(event)
241
+ : null}
242
+ style="height: calc(100% - {selected_media.caption
243
+ ? '80px'
244
+ : '60px'})"
245
+ aria-label="detailed view of selected image"
246
+ >
247
+ {#if "image" in selected_media}
248
+ <Image
249
+ data-testid="detailed-image"
250
+ src={selected_media.image.url}
251
+ alt={selected_media.caption || ""}
252
+ title={selected_media.caption || null}
253
+ class={selected_media.caption && "with-caption"}
254
+ loading="lazy"
255
+ />
256
+ {:else}
257
+ <Video
258
+ src={selected_media.video.url}
259
+ data-testid={"detailed-video"}
260
+ alt={selected_media.caption || ""}
261
+ loading="lazy"
262
+ loop={false}
263
+ is_stream={false}
264
+ muted={false}
265
+ controls={true}
266
+ />
267
+ {/if}
268
+ </button>
269
+ {#if selected_media?.caption}
270
+ <caption class="caption">
271
+ {selected_media.caption}
272
+ </caption>
273
+ {/if}
274
+ <div
275
+ bind:this={container_element}
276
+ class="thumbnails scroll-hide"
277
+ data-testid="container_el"
278
+ >
279
+ {#each resolved_value as media, i}
280
+ <button
281
+ bind:this={el[i]}
282
+ on:click={() => (selected_index = i)}
283
+ class="thumbnail-item thumbnail-small"
284
+ class:selected={selected_index === i && mode !== "minimal"}
285
+ aria-label={"Thumbnail " +
286
+ (i + 1) +
287
+ " of " +
288
+ resolved_value.length}
289
+ >
290
+ {#if "image" in media}
291
+ <Image
292
+ src={media.image.url}
293
+ title={media.caption || null}
294
+ data-testid={"thumbnail " + (i + 1)}
295
+ alt=""
296
+ loading="lazy"
297
+ />
298
+ {:else}
299
+ <Play />
300
+ <Video
301
+ src={media.video.url}
302
+ title={media.caption || null}
303
+ is_stream={false}
304
+ data-testid={"thumbnail " + (i + 1)}
305
+ alt=""
306
+ loading="lazy"
307
+ loop={false}
308
+ />
309
+ {/if}
310
+ </button>
311
+ {/each}
312
+ </div>
313
+ </button>
314
+ {/if}
315
+
316
+ <div
317
+ class="grid-wrap"
318
+ class:minimal={mode === "minimal"}
319
+ class:fixed-height={mode !== "minimal" && (!height || height == "auto")}
320
+ class:hidden={is_full_screen}
321
+ >
322
+ <div
323
+ class="grid-container"
324
+ style="--grid-cols:{columns}; --grid-rows:{rows}; --object-fit: {object_fit}; height: {height};"
325
+ class:pt-6={show_label}
326
+ >
327
+ {#if interactive}
328
+ <div class="icon-button">
329
+ <ModifyUpload
330
+ {i18n}
331
+ absolute={false}
332
+ on:clear={() => (value = [])}
333
+ />
334
+ </div>
335
+ {/if}
336
+ {#if show_share_button}
337
+ <div class="icon-button">
338
+ <ShareButton
339
+ {i18n}
340
+ on:share
341
+ on:error
342
+ value={resolved_value}
343
+ formatter={format_gallery_for_sharing}
344
+ />
345
+ </div>
346
+ {/if}
347
+ {#each resolved_value as entry, i}
348
+ <button
349
+ class="thumbnail-item thumbnail-lg"
350
+ class:selected={selected_index === i}
351
+ on:click={() => (selected_index = i)}
352
+ aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
353
+ >
354
+ {#if "image" in entry}
355
+ <Image
356
+ alt={entry.caption || ""}
357
+ src={typeof entry.image === "string"
358
+ ? entry.image
359
+ : entry.image.url}
360
+ loading="lazy"
361
+ />
362
+ {:else}
363
+ <Play />
364
+ <Video
365
+ src={entry.video.url}
366
+ title={entry.caption || null}
367
+ is_stream={false}
368
+ data-testid={"thumbnail " + (i + 1)}
369
+ alt=""
370
+ loading="lazy"
371
+ loop={false}
372
+ />
373
+ {/if}
374
+ {#if entry.caption}
375
+ <div class="caption-label">
376
+ {entry.caption}
377
+ </div>
378
+ {/if}
379
+ </button>
380
+ {/each}
381
+ </div>
382
+ </div>
383
+ </div>
384
+ {/if}
385
+
386
+ <style>
387
+ .image-container {
388
+ height: 100%;
389
+ position: relative;
390
+ }
391
+ .image-container :global(img),
392
+ button {
393
+ width: var(--size-full);
394
+ height: var(--size-full);
395
+ object-fit: contain;
396
+ display: block;
397
+ border-radius: var(--radius-lg);
398
+ }
399
+
400
+ .preview {
401
+ display: flex;
402
+ position: absolute;
403
+ flex-direction: column;
404
+ z-index: var(--layer-2);
405
+ border-radius: calc(var(--block-radius) - var(--block-border-width));
406
+ -webkit-backdrop-filter: blur(8px);
407
+ backdrop-filter: blur(8px);
408
+ width: var(--size-full);
409
+ height: var(--size-full);
410
+ }
411
+
412
+ .preview.minimal {
413
+ width: fit-content;
414
+ height: fit-content;
415
+ }
416
+
417
+ .preview::before {
418
+ content: "";
419
+ position: absolute;
420
+ z-index: var(--layer-below);
421
+ background: var(--background-fill-primary);
422
+ opacity: 0.9;
423
+ width: var(--size-full);
424
+ height: var(--size-full);
425
+ }
426
+
427
+ .fixed-height {
428
+ min-height: var(--size-80);
429
+ max-height: 55vh;
430
+ }
431
+
432
+ @media (--screen-xl) {
433
+ .fixed-height {
434
+ min-height: 450px;
435
+ }
436
+ }
437
+
438
+ .media-button {
439
+ height: calc(100% - 60px);
440
+ width: 100%;
441
+ display: flex;
442
+ }
443
+ .media-button :global(img),
444
+ .media-button :global(video) {
445
+ width: var(--size-full);
446
+ height: var(--size-full);
447
+ object-fit: contain;
448
+ }
449
+ .thumbnails :global(img) {
450
+ object-fit: cover;
451
+ width: var(--size-full);
452
+ height: var(--size-full);
453
+ }
454
+ .thumbnails :global(svg) {
455
+ position: absolute;
456
+ top: var(--size-2);
457
+ left: var(--size-2);
458
+ width: 50%;
459
+ height: 50%;
460
+ opacity: 50%;
461
+ }
462
+ .preview :global(img.with-caption) {
463
+ height: var(--size-full);
464
+ }
465
+
466
+ .preview.minimal :global(img.with-caption) {
467
+ height: auto;
468
+ }
469
+
470
+ .selectable {
471
+ cursor: crosshair;
472
+ }
473
+
474
+ .caption {
475
+ padding: var(--size-2) var(--size-3);
476
+ overflow: hidden;
477
+ color: var(--block-label-text-color);
478
+ font-weight: var(--weight-semibold);
479
+ text-align: center;
480
+ text-overflow: ellipsis;
481
+ white-space: nowrap;
482
+ align-self: center;
483
+ }
484
+
485
+ .thumbnails {
486
+ display: flex;
487
+ position: absolute;
488
+ bottom: 0;
489
+ justify-content: center;
490
+ align-items: center;
491
+ gap: var(--spacing-lg);
492
+ width: var(--size-full);
493
+ height: var(--size-14);
494
+ overflow-x: scroll;
495
+ }
496
+
497
+ .thumbnail-item {
498
+ --ring-color: transparent;
499
+ position: relative;
500
+ box-shadow:
501
+ inset 0 0 0 1px var(--ring-color),
502
+ var(--shadow-drop);
503
+ border: 1px solid var(--border-color-primary);
504
+ border-radius: var(--button-small-radius);
505
+ background: var(--background-fill-secondary);
506
+ aspect-ratio: var(--ratio-square);
507
+ width: var(--size-full);
508
+ height: var(--size-full);
509
+ overflow: clip;
510
+ }
511
+
512
+ .thumbnail-item:hover {
513
+ --ring-color: var(--color-accent);
514
+ border-color: var(--color-accent);
515
+ filter: brightness(1.1);
516
+ }
517
+
518
+ .thumbnail-item.selected {
519
+ --ring-color: var(--color-accent);
520
+ border-color: var(--color-accent);
521
+ }
522
+
523
+ .thumbnail-item :global(svg) {
524
+ position: absolute;
525
+ top: 50%;
526
+ left: 50%;
527
+ width: 50%;
528
+ height: 50%;
529
+ opacity: 50%;
530
+ transform: translate(-50%, -50%);
531
+ }
532
+
533
+ .thumbnail-item :global(video) {
534
+ width: var(--size-full);
535
+ height: var(--size-full);
536
+ overflow: hidden;
537
+ object-fit: cover;
538
+ }
539
+
540
+ .thumbnail-small {
541
+ flex: none;
542
+ transform: scale(0.9);
543
+ transition: 0.075s;
544
+ width: var(--size-9);
545
+ height: var(--size-9);
546
+ }
547
+ .thumbnail-small.selected {
548
+ --ring-color: var(--color-accent);
549
+ transform: scale(1);
550
+ border-color: var(--color-accent);
551
+ }
552
+
553
+ .thumbnail-small > img {
554
+ width: var(--size-full);
555
+ height: var(--size-full);
556
+ overflow: hidden;
557
+ object-fit: var(--object-fit);
558
+ }
559
+
560
+ .grid-wrap {
561
+ position: relative;
562
+ padding: var(--size-2);
563
+ height: var(--size-full);
564
+ overflow-y: scroll;
565
+ }
566
+
567
+ .grid-container {
568
+ display: grid;
569
+ position: relative;
570
+ grid-template-rows: repeat(var(--grid-rows), minmax(100px, 1fr));
571
+ grid-template-columns: repeat(var(--grid-cols), minmax(100px, 1fr));
572
+ grid-auto-rows: minmax(100px, 1fr);
573
+ gap: var(--spacing-lg);
574
+ }
575
+
576
+ .thumbnail-lg > :global(img) {
577
+ width: var(--size-full);
578
+ height: var(--size-full);
579
+ overflow: hidden;
580
+ object-fit: var(--object-fit);
581
+ }
582
+
583
+ .thumbnail-lg:hover .caption-label {
584
+ opacity: 0.5;
585
+ }
586
+
587
+ .caption-label {
588
+ position: absolute;
589
+ right: var(--block-label-margin);
590
+ bottom: var(--block-label-margin);
591
+ z-index: var(--layer-1);
592
+ border-top: 1px solid var(--border-color-primary);
593
+ border-left: 1px solid var(--border-color-primary);
594
+ border-radius: var(--block-label-radius);
595
+ background: var(--background-fill-secondary);
596
+ padding: var(--block-label-padding);
597
+ max-width: 80%;
598
+ overflow: hidden;
599
+ font-size: var(--block-label-text-size);
600
+ text-align: left;
601
+ text-overflow: ellipsis;
602
+ white-space: nowrap;
603
+ }
604
+
605
+ .icon-button {
606
+ position: absolute;
607
+ top: 0px;
608
+ right: 0px;
609
+ z-index: var(--layer-1);
610
+ }
611
+
612
+ .icon-buttons {
613
+ display: flex;
614
+ position: absolute;
615
+ right: 0;
616
+ gap: var(--size-1);
617
+ z-index: 1;
618
+ margin: var(--size-1);
619
+ }
620
+
621
+ .grid-wrap.minimal {
622
+ padding: 0;
623
+ }
624
+ </style>
@@ -0,0 +1,40 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { SelectData } from "@gradio/utils";
3
+ import type { GalleryImage, GalleryVideo } from "../types";
4
+ import type { I18nFormatter } from "@gradio/utils";
5
+ declare const __propDef: {
6
+ props: {
7
+ show_label?: boolean | undefined;
8
+ label: string;
9
+ value?: ((GalleryImage | GalleryVideo)[] | null) | undefined;
10
+ columns?: number | number[] | undefined;
11
+ rows?: number | number[] | undefined;
12
+ height?: (number | "auto") | undefined;
13
+ preview: boolean;
14
+ allow_preview?: boolean | undefined;
15
+ object_fit?: ("contain" | "cover" | "fill" | "none" | "scale-down") | undefined;
16
+ show_share_button?: boolean | undefined;
17
+ show_download_button?: boolean | undefined;
18
+ i18n: I18nFormatter;
19
+ selected_index?: (number | null) | undefined;
20
+ interactive: boolean;
21
+ _fetch: typeof fetch;
22
+ mode?: ("normal" | "minimal") | undefined;
23
+ show_fullscreen_button?: boolean | undefined;
24
+ };
25
+ events: {
26
+ share: CustomEvent<import("@gradio/utils").ShareData>;
27
+ error: CustomEvent<string>;
28
+ change: CustomEvent<undefined>;
29
+ select: CustomEvent<SelectData>;
30
+ } & {
31
+ [evt: string]: CustomEvent<any>;
32
+ };
33
+ slots: {};
34
+ };
35
+ export type GalleryProps = typeof __propDef.props;
36
+ export type GalleryEvents = typeof __propDef.events;
37
+ export type GallerySlots = typeof __propDef.slots;
38
+ export default class Gallery extends SvelteComponent<GalleryProps, GalleryEvents, GallerySlots> {
39
+ }
40
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { FileData } from "@gradio/client";
2
+ export declare function format_gallery_for_sharing(value: [FileData, string | null][] | null): Promise<string>;
@@ -0,0 +1,13 @@
1
+ import { uploadToHuggingFace } from "@gradio/utils";
2
+ export async function format_gallery_for_sharing(value) {
3
+ if (!value)
4
+ return "";
5
+ let urls = await Promise.all(value.map(async ([image, _]) => {
6
+ if (image === null || !image.url)
7
+ return "";
8
+ return await uploadToHuggingFace(image.url, "url");
9
+ }));
10
+ return `<div style="display: flex; flex-wrap: wrap; gap: 16px">${urls
11
+ .map((url) => `<img src="${url}" style="height: 400px" />`)
12
+ .join("")}</div>`;
13
+ }
@@ -0,0 +1,9 @@
1
+ import type { FileData } from "@gradio/client";
2
+ export interface GalleryImage {
3
+ image: FileData;
4
+ caption: string | null;
5
+ }
6
+ export interface GalleryVideo {
7
+ video: FileData;
8
+ caption: string | null;
9
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};