@gradio/gallery 0.15.35-dev.0 → 0.15.35-dev.3

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.
@@ -1,230 +1,316 @@
1
- <script>import {
2
- BlockLabel,
3
- Empty,
4
- ShareButton,
5
- IconButton,
6
- IconButtonWrapper,
7
- FullscreenButton
8
- } from "@gradio/atoms";
9
- import { ModifyUpload, Upload as UploadComponent } from "@gradio/upload";
10
- import { Image } from "@gradio/image/shared";
11
- import { Video } from "@gradio/video/shared";
12
- import { dequal } from "dequal";
13
- import { createEventDispatcher, onMount } from "svelte";
14
- import { tick } from "svelte";
15
- import {
16
- Download,
17
- Image as ImageIcon,
18
- Clear,
19
- Play,
20
- Upload as UploadIcon
21
- } from "@gradio/icons";
22
- import { FileData } from "@gradio/client";
23
- import { format_gallery_for_sharing } from "./utils";
24
- export let show_label = true;
25
- export let label;
26
- export let value = null;
27
- export let columns = [2];
28
- export let rows = void 0;
29
- export let height = "auto";
30
- export let preview;
31
- export let allow_preview = true;
32
- export let object_fit = "cover";
33
- export let show_share_button = false;
34
- export let show_download_button = false;
35
- export let i18n;
36
- export let selected_index = null;
37
- export let interactive;
38
- export let _fetch;
39
- export let mode = "normal";
40
- export let show_fullscreen_button = true;
41
- export let display_icon_button_wrapper_top_corner = false;
42
- export let fullscreen = false;
43
- export let root = "";
44
- export let file_types = ["image", "video"];
45
- export let max_file_size = null;
46
- export let upload = void 0;
47
- export let stream_handler = void 0;
48
- let is_full_screen = false;
49
- let image_container;
50
- const dispatch = createEventDispatcher();
51
- let was_reset = true;
52
- $: was_reset = value == null || value.length === 0 ? true : was_reset;
53
- let resolved_value = null;
54
- $: resolved_value = value == null ? null : value.map((data) => {
55
- if ("video" in data) {
56
- return {
57
- video: data.video,
58
- caption: data.caption
59
- };
60
- } else if ("image" in data) {
61
- return { image: data.image, caption: data.caption };
62
- }
63
- return {};
64
- });
65
- let effective_columns = columns;
66
- $: {
67
- if (resolved_value && columns) {
68
- const item_count = resolved_value.length;
69
- if (Array.isArray(columns)) {
70
- effective_columns = columns.map((col) => Math.min(col, item_count));
71
- } else {
72
- effective_columns = Math.min(columns, item_count);
73
- }
74
- } else {
75
- effective_columns = columns;
76
- }
77
- }
78
- let prev_value = value;
79
- if (selected_index == null && preview && value?.length) {
80
- selected_index = 0;
81
- }
82
- let old_selected_index = selected_index;
83
- $: if (!dequal(prev_value, value)) {
84
- if (was_reset) {
85
- selected_index = preview && value?.length ? 0 : null;
86
- was_reset = false;
87
- } else {
88
- if (selected_index !== null && value !== null) {
89
- selected_index = Math.max(
90
- 0,
91
- Math.min(selected_index, value.length - 1)
92
- );
93
- } else {
94
- selected_index = null;
95
- }
96
- }
97
- dispatch("change");
98
- prev_value = value;
99
- }
100
- $: previous = ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) % (resolved_value?.length ?? 0);
101
- $: next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
102
- function handle_preview_click(event) {
103
- const element = event.target;
104
- const x = event.offsetX;
105
- const width = element.offsetWidth;
106
- const centerX = width / 2;
107
- if (x < centerX) {
108
- selected_index = previous;
109
- } else {
110
- selected_index = next;
111
- }
112
- }
113
- function on_keydown(e) {
114
- switch (e.code) {
115
- case "Escape":
116
- e.preventDefault();
117
- selected_index = null;
118
- break;
119
- case "ArrowLeft":
120
- e.preventDefault();
121
- selected_index = previous;
122
- break;
123
- case "ArrowRight":
124
- e.preventDefault();
125
- selected_index = next;
126
- break;
127
- default:
128
- break;
129
- }
130
- }
131
- $: {
132
- if (selected_index !== old_selected_index) {
133
- old_selected_index = selected_index;
134
- if (selected_index !== null) {
135
- if (resolved_value != null) {
136
- selected_index = Math.max(
137
- 0,
138
- Math.min(selected_index, resolved_value.length - 1)
139
- );
140
- }
141
- dispatch("select", {
142
- index: selected_index,
143
- value: resolved_value?.[selected_index]
144
- });
145
- }
146
- }
147
- }
148
- $: if (allow_preview) {
149
- scroll_to_img(selected_index);
150
- }
151
- let el = [];
152
- let container_element;
153
- async function scroll_to_img(index) {
154
- if (typeof index !== "number") return;
155
- await tick();
156
- if (el[index] === void 0) return;
157
- el[index]?.focus();
158
- const { left: container_left, width: container_width } = container_element.getBoundingClientRect();
159
- const { left, width } = el[index].getBoundingClientRect();
160
- const relative_left = left - container_left;
161
- const pos = relative_left + width / 2 - container_width / 2 + container_element.scrollLeft;
162
- if (container_element && typeof container_element.scrollTo === "function") {
163
- container_element.scrollTo({
164
- left: pos < 0 ? 0 : pos,
165
- behavior: "smooth"
166
- });
167
- }
168
- }
169
- let window_height = 0;
170
- async function download(file_url, name) {
171
- let response;
172
- try {
173
- response = await _fetch(file_url);
174
- } catch (error) {
175
- if (error instanceof TypeError) {
176
- window.open(file_url, "_blank", "noreferrer");
177
- return;
178
- }
179
- throw error;
180
- }
181
- const blob = await response.blob();
182
- const url = URL.createObjectURL(blob);
183
- const link = document.createElement("a");
184
- link.href = url;
185
- link.download = name;
186
- link.click();
187
- URL.revokeObjectURL(url);
188
- }
189
- $: selected_media = selected_index != null && resolved_value != null ? resolved_value[selected_index] : null;
190
- let thumbnails_overflow = false;
191
- function check_thumbnails_overflow() {
192
- if (container_element) {
193
- thumbnails_overflow = container_element.scrollWidth > container_element.clientWidth;
194
- }
195
- }
196
- onMount(() => {
197
- check_thumbnails_overflow();
198
- document.addEventListener("fullscreenchange", () => {
199
- is_full_screen = !!document.fullscreenElement;
200
- });
201
- window.addEventListener("resize", check_thumbnails_overflow);
202
- return () => window.removeEventListener("resize", check_thumbnails_overflow);
203
- });
204
- $: resolved_value, check_thumbnails_overflow();
205
- $: if (container_element) {
206
- check_thumbnails_overflow();
207
- }
208
- function handle_item_delete(index) {
209
- if (!value || !resolved_value) return;
210
- const deleted_item = resolved_value[index];
211
- let deleted_file_data;
212
- if ("image" in deleted_item) {
213
- deleted_file_data = {
214
- file: deleted_item.image,
215
- index
216
- };
217
- } else if ("video" in deleted_item) {
218
- deleted_file_data = {
219
- file: deleted_item.video,
220
- index
221
- };
222
- }
223
- if (deleted_file_data) {
224
- dispatch("delete", deleted_file_data);
225
- }
226
- }
227
- let uploading = false;
1
+ <script lang="ts">
2
+ import {
3
+ BlockLabel,
4
+ Empty,
5
+ ShareButton,
6
+ IconButton,
7
+ IconButtonWrapper,
8
+ FullscreenButton
9
+ } from "@gradio/atoms";
10
+ import { ModifyUpload, Upload as UploadComponent } from "@gradio/upload";
11
+ import type { SelectData } from "@gradio/utils";
12
+ import { Image } from "@gradio/image/shared";
13
+ import { Video } from "@gradio/video/shared";
14
+ import { dequal } from "dequal";
15
+ import { createEventDispatcher, onMount } from "svelte";
16
+ import { tick } from "svelte";
17
+ import type { GalleryImage, GalleryVideo } from "../types";
18
+
19
+ import {
20
+ Download,
21
+ Image as ImageIcon,
22
+ Clear,
23
+ Play,
24
+ Upload as UploadIcon
25
+ } from "@gradio/icons";
26
+ import { FileData } from "@gradio/client";
27
+ import type { Client } from "@gradio/client";
28
+ import { format_gallery_for_sharing } from "./utils";
29
+ import type { I18nFormatter } from "@gradio/utils";
30
+
31
+ type GalleryData = GalleryImage | GalleryVideo;
32
+
33
+ export let show_label = true;
34
+ export let label: string;
35
+ export let value: GalleryData[] | null = null;
36
+ export let columns: number | number[] | undefined = [2];
37
+ export let rows: number | number[] | undefined = undefined;
38
+ export let height: number | "auto" = "auto";
39
+ export let preview: boolean;
40
+ export let allow_preview = true;
41
+ export let object_fit: "contain" | "cover" | "fill" | "none" | "scale-down" =
42
+ "cover";
43
+ export let show_share_button = false;
44
+ export let show_download_button = false;
45
+ export let i18n: I18nFormatter;
46
+ export let selected_index: number | null = null;
47
+ export let interactive: boolean;
48
+ export let _fetch: typeof fetch;
49
+ export let mode: "normal" | "minimal" = "normal";
50
+ export let show_fullscreen_button = true;
51
+ export let display_icon_button_wrapper_top_corner = false;
52
+ export let fullscreen = false;
53
+ export let root = "";
54
+ export let file_types: string[] | null = ["image", "video"];
55
+ export let max_file_size: number | null = null;
56
+ export let upload: Client["upload"] | undefined = undefined;
57
+ export let stream_handler: Client["stream"] | undefined = undefined;
58
+ export let fit_columns = true;
59
+ export let upload_promise: Promise<any> | null = null;
60
+
61
+ let is_full_screen = false;
62
+ let image_container: HTMLElement;
63
+
64
+ const dispatch = createEventDispatcher<{
65
+ change: undefined;
66
+ select: SelectData;
67
+ preview_open: undefined;
68
+ preview_close: undefined;
69
+ fullscreen: boolean;
70
+ delete: { file: FileData; index: number };
71
+ upload: FileData | FileData[];
72
+ error: string;
73
+ clear: undefined;
74
+ }>();
75
+
76
+ // tracks whether the value of the gallery was reset
77
+ let was_reset = true;
78
+
79
+ $: was_reset = value == null || value.length === 0 ? true : was_reset;
80
+
81
+ let resolved_value: GalleryData[] | null = null;
82
+
83
+ $: resolved_value =
84
+ value == null
85
+ ? null
86
+ : (value.map((data) => {
87
+ if ("video" in data) {
88
+ return {
89
+ video: data.video as FileData,
90
+ caption: data.caption
91
+ };
92
+ } else if ("image" in data) {
93
+ return { image: data.image as FileData, caption: data.caption };
94
+ }
95
+ return {};
96
+ }) as GalleryData[]);
97
+
98
+ let effective_columns: number | number[] | undefined = columns;
99
+
100
+ $: {
101
+ if (resolved_value && columns && fit_columns) {
102
+ const item_count = resolved_value.length;
103
+ if (Array.isArray(columns)) {
104
+ effective_columns = columns.map((col) => Math.min(col, item_count));
105
+ } else {
106
+ effective_columns = Math.min(columns, item_count);
107
+ }
108
+ } else {
109
+ effective_columns = columns;
110
+ }
111
+ }
112
+
113
+ let prev_value: GalleryData[] | null = value;
114
+ if (selected_index == null && preview && value?.length) {
115
+ selected_index = 0;
116
+ }
117
+ let old_selected_index: number | null = selected_index;
118
+
119
+ $: if (!dequal(prev_value, value)) {
120
+ // When value is falsy (clear button or first load),
121
+ // preview determines the selected image
122
+ if (was_reset) {
123
+ selected_index = preview && value?.length ? 0 : null;
124
+ was_reset = false;
125
+ // Otherwise we keep the selected_index the same if the
126
+ // gallery has at least as many elements as it did before
127
+ } else {
128
+ if (selected_index !== null && value !== null) {
129
+ selected_index = Math.max(
130
+ 0,
131
+ Math.min(selected_index, value.length - 1)
132
+ );
133
+ } else {
134
+ selected_index = null;
135
+ }
136
+ }
137
+ dispatch("change");
138
+ prev_value = value;
139
+ }
140
+
141
+ $: previous =
142
+ ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) %
143
+ (resolved_value?.length ?? 0);
144
+ $: next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
145
+
146
+ function handle_preview_click(event: MouseEvent): void {
147
+ const element = event.target as HTMLElement;
148
+ const x = event.offsetX;
149
+ const width = element.offsetWidth;
150
+ const centerX = width / 2;
151
+
152
+ if (x < centerX) {
153
+ selected_index = previous;
154
+ } else {
155
+ selected_index = next;
156
+ }
157
+ }
158
+
159
+ function on_keydown(e: KeyboardEvent): void {
160
+ switch (e.code) {
161
+ case "Escape":
162
+ e.preventDefault();
163
+ selected_index = null;
164
+ break;
165
+ case "ArrowLeft":
166
+ e.preventDefault();
167
+ selected_index = previous;
168
+ break;
169
+ case "ArrowRight":
170
+ e.preventDefault();
171
+ selected_index = next;
172
+ break;
173
+ default:
174
+ break;
175
+ }
176
+ }
177
+
178
+ $: {
179
+ if (selected_index !== old_selected_index) {
180
+ old_selected_index = selected_index;
181
+ if (selected_index !== null) {
182
+ if (resolved_value != null) {
183
+ selected_index = Math.max(
184
+ 0,
185
+ Math.min(selected_index, resolved_value.length - 1)
186
+ );
187
+ }
188
+ dispatch("select", {
189
+ index: selected_index,
190
+ value: resolved_value?.[selected_index]
191
+ });
192
+ }
193
+ }
194
+ }
195
+
196
+ $: if (allow_preview) {
197
+ scroll_to_img(selected_index);
198
+ }
199
+
200
+ let el: HTMLButtonElement[] = [];
201
+ let container_element: HTMLDivElement;
202
+
203
+ async function scroll_to_img(index: number | null): Promise<void> {
204
+ if (typeof index !== "number") return;
205
+ await tick();
206
+
207
+ if (el[index] === undefined) return;
208
+
209
+ el[index]?.focus();
210
+
211
+ const { left: container_left, width: container_width } =
212
+ container_element.getBoundingClientRect();
213
+ const { left, width } = el[index].getBoundingClientRect();
214
+
215
+ const relative_left = left - container_left;
216
+
217
+ const pos =
218
+ relative_left +
219
+ width / 2 -
220
+ container_width / 2 +
221
+ container_element.scrollLeft;
222
+
223
+ if (container_element && typeof container_element.scrollTo === "function") {
224
+ container_element.scrollTo({
225
+ left: pos < 0 ? 0 : pos,
226
+ behavior: "smooth"
227
+ });
228
+ }
229
+ }
230
+
231
+ let window_height = 0;
232
+
233
+ // Unlike `gr.Image()`, images specified via remote URLs are not cached in the server
234
+ // and their remote URLs are directly passed to the client as `value[].image.url`.
235
+ // The `download` attribute of the <a> tag doesn't work for remote URLs (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download),
236
+ // so we need to download the image via JS as below.
237
+ async function download(file_url: string, name: string): Promise<void> {
238
+ let response;
239
+ try {
240
+ response = await _fetch(file_url);
241
+ } catch (error) {
242
+ if (error instanceof TypeError) {
243
+ // If CORS is not allowed (https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#checking_that_the_fetch_was_successful),
244
+ // open the link in a new tab instead, mimicing the behavior of the `download` attribute for remote URLs,
245
+ // which is not ideal, but a reasonable fallback.
246
+ window.open(file_url, "_blank", "noreferrer");
247
+ return;
248
+ }
249
+
250
+ throw error;
251
+ }
252
+ const blob = await response.blob();
253
+ const url = URL.createObjectURL(blob);
254
+ const link = document.createElement("a");
255
+ link.href = url;
256
+ link.download = name;
257
+ link.click();
258
+ URL.revokeObjectURL(url);
259
+ }
260
+
261
+ $: selected_media =
262
+ selected_index != null && resolved_value != null
263
+ ? resolved_value[selected_index]
264
+ : null;
265
+
266
+ let thumbnails_overflow = false;
267
+
268
+ function check_thumbnails_overflow(): void {
269
+ if (container_element) {
270
+ thumbnails_overflow =
271
+ container_element.scrollWidth > container_element.clientWidth;
272
+ }
273
+ }
274
+
275
+ onMount(() => {
276
+ check_thumbnails_overflow();
277
+ document.addEventListener("fullscreenchange", () => {
278
+ is_full_screen = !!document.fullscreenElement;
279
+ });
280
+ window.addEventListener("resize", check_thumbnails_overflow);
281
+ return () =>
282
+ window.removeEventListener("resize", check_thumbnails_overflow);
283
+ });
284
+
285
+ $: (resolved_value, check_thumbnails_overflow());
286
+ $: if (container_element) {
287
+ check_thumbnails_overflow();
288
+ }
289
+
290
+ function handle_item_delete(index: number): void {
291
+ if (!value || !resolved_value) return;
292
+
293
+ const deleted_item = resolved_value[index];
294
+ let deleted_file_data;
295
+
296
+ if ("image" in deleted_item) {
297
+ deleted_file_data = {
298
+ file: deleted_item.image,
299
+ index: index
300
+ };
301
+ } else if ("video" in deleted_item) {
302
+ deleted_file_data = {
303
+ file: deleted_item.video,
304
+ index: index
305
+ };
306
+ }
307
+
308
+ if (deleted_file_data) {
309
+ dispatch("delete", deleted_file_data);
310
+ }
311
+ }
312
+
313
+ let uploading = false;
228
314
  </script>
229
315
 
230
316
  <svelte:window bind:innerHeight={window_height} />
@@ -237,7 +323,7 @@ let uploading = false;
237
323
  {:else}
238
324
  <div class="gallery-container" bind:this={image_container}>
239
325
  {#if selected_media && allow_preview}
240
- <button
326
+ <span
241
327
  on:keydown={on_keydown}
242
328
  class="preview"
243
329
  class:minimal={mode === "minimal"}
@@ -303,12 +389,14 @@ let uploading = false;
303
389
  >
304
390
  {#if "image" in selected_media}
305
391
  <Image
306
- data-testid="detailed-image"
392
+ restProps={{
393
+ alt: selected_media.caption || "",
394
+ title: selected_media.caption || null,
395
+ class: selected_media.caption && "with-caption",
396
+ loading: "lazy"
397
+ }}
307
398
  src={selected_media.image.url}
308
- alt={selected_media.caption || ""}
309
- title={selected_media.caption || null}
310
- class={selected_media.caption && "with-caption"}
311
- loading="lazy"
399
+ data_testid="detailed-image"
312
400
  />
313
401
  {:else}
314
402
  <Video
@@ -350,10 +438,13 @@ let uploading = false;
350
438
  {#if "image" in media}
351
439
  <Image
352
440
  src={media.image.url}
353
- title={media.caption || null}
354
- data-testid={"thumbnail " + (i + 1)}
355
- alt=""
356
- loading="lazy"
441
+ restProps={{
442
+ title: media.caption || null,
443
+ alt: "",
444
+ class: "with-caption",
445
+ loading: "lazy"
446
+ }}
447
+ data_testid={`thumbnail ${i + 1}`}
357
448
  />
358
449
  {:else}
359
450
  <Play />
@@ -370,7 +461,7 @@ let uploading = false;
370
461
  </button>
371
462
  {/each}
372
463
  </div>
373
- </button>
464
+ </span>
374
465
  {/if}
375
466
 
376
467
  <div
@@ -381,10 +472,17 @@ let uploading = false;
381
472
  style:height={height !== "auto" ? height + "px" : null}
382
473
  >
383
474
  {#if interactive && selected_index === null}
384
- <ModifyUpload {i18n} on:clear={() => (value = [])}>
475
+ <ModifyUpload
476
+ {i18n}
477
+ on:clear={() => {
478
+ value = [];
479
+ dispatch("clear");
480
+ }}
481
+ >
385
482
  {#if upload && stream_handler}
386
483
  <IconButton Icon={UploadIcon} label={i18n("common.upload")}>
387
484
  <UploadComponent
485
+ bind:upload_promise
388
486
  icon_upload={true}
389
487
  on:load={(e) => dispatch("upload", e.detail)}
390
488
  filetype={file_types}
@@ -463,7 +561,7 @@ let uploading = false;
463
561
  </div>
464
562
  {/if}
465
563
 
466
- <style>
564
+ <style lang="postcss">
467
565
  .image-container {
468
566
  height: 100%;
469
567
  position: relative;