@gradio/gallery 0.15.35-dev.1 → 0.15.35

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,231 +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
- export let fit_columns = true;
49
- let is_full_screen = false;
50
- let image_container;
51
- const dispatch = createEventDispatcher();
52
- let was_reset = true;
53
- $: was_reset = value == null || value.length === 0 ? true : was_reset;
54
- let resolved_value = null;
55
- $: resolved_value = value == null ? null : value.map((data) => {
56
- if ("video" in data) {
57
- return {
58
- video: data.video,
59
- caption: data.caption
60
- };
61
- } else if ("image" in data) {
62
- return { image: data.image, caption: data.caption };
63
- }
64
- return {};
65
- });
66
- let effective_columns = columns;
67
- $: {
68
- if (resolved_value && columns && fit_columns) {
69
- const item_count = resolved_value.length;
70
- if (Array.isArray(columns)) {
71
- effective_columns = columns.map((col) => Math.min(col, item_count));
72
- } else {
73
- effective_columns = Math.min(columns, item_count);
74
- }
75
- } else {
76
- effective_columns = columns;
77
- }
78
- }
79
- let prev_value = value;
80
- if (selected_index == null && preview && value?.length) {
81
- selected_index = 0;
82
- }
83
- let old_selected_index = selected_index;
84
- $: if (!dequal(prev_value, value)) {
85
- if (was_reset) {
86
- selected_index = preview && value?.length ? 0 : null;
87
- was_reset = false;
88
- } else {
89
- if (selected_index !== null && value !== null) {
90
- selected_index = Math.max(
91
- 0,
92
- Math.min(selected_index, value.length - 1)
93
- );
94
- } else {
95
- selected_index = null;
96
- }
97
- }
98
- dispatch("change");
99
- prev_value = value;
100
- }
101
- $: previous = ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) % (resolved_value?.length ?? 0);
102
- $: next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
103
- function handle_preview_click(event) {
104
- const element = event.target;
105
- const x = event.offsetX;
106
- const width = element.offsetWidth;
107
- const centerX = width / 2;
108
- if (x < centerX) {
109
- selected_index = previous;
110
- } else {
111
- selected_index = next;
112
- }
113
- }
114
- function on_keydown(e) {
115
- switch (e.code) {
116
- case "Escape":
117
- e.preventDefault();
118
- selected_index = null;
119
- break;
120
- case "ArrowLeft":
121
- e.preventDefault();
122
- selected_index = previous;
123
- break;
124
- case "ArrowRight":
125
- e.preventDefault();
126
- selected_index = next;
127
- break;
128
- default:
129
- break;
130
- }
131
- }
132
- $: {
133
- if (selected_index !== old_selected_index) {
134
- old_selected_index = selected_index;
135
- if (selected_index !== null) {
136
- if (resolved_value != null) {
137
- selected_index = Math.max(
138
- 0,
139
- Math.min(selected_index, resolved_value.length - 1)
140
- );
141
- }
142
- dispatch("select", {
143
- index: selected_index,
144
- value: resolved_value?.[selected_index]
145
- });
146
- }
147
- }
148
- }
149
- $: if (allow_preview) {
150
- scroll_to_img(selected_index);
151
- }
152
- let el = [];
153
- let container_element;
154
- async function scroll_to_img(index) {
155
- if (typeof index !== "number") return;
156
- await tick();
157
- if (el[index] === void 0) return;
158
- el[index]?.focus();
159
- const { left: container_left, width: container_width } = container_element.getBoundingClientRect();
160
- const { left, width } = el[index].getBoundingClientRect();
161
- const relative_left = left - container_left;
162
- const pos = relative_left + width / 2 - container_width / 2 + container_element.scrollLeft;
163
- if (container_element && typeof container_element.scrollTo === "function") {
164
- container_element.scrollTo({
165
- left: pos < 0 ? 0 : pos,
166
- behavior: "smooth"
167
- });
168
- }
169
- }
170
- let window_height = 0;
171
- async function download(file_url, name) {
172
- let response;
173
- try {
174
- response = await _fetch(file_url);
175
- } catch (error) {
176
- if (error instanceof TypeError) {
177
- window.open(file_url, "_blank", "noreferrer");
178
- return;
179
- }
180
- throw error;
181
- }
182
- const blob = await response.blob();
183
- const url = URL.createObjectURL(blob);
184
- const link = document.createElement("a");
185
- link.href = url;
186
- link.download = name;
187
- link.click();
188
- URL.revokeObjectURL(url);
189
- }
190
- $: selected_media = selected_index != null && resolved_value != null ? resolved_value[selected_index] : null;
191
- let thumbnails_overflow = false;
192
- function check_thumbnails_overflow() {
193
- if (container_element) {
194
- thumbnails_overflow = container_element.scrollWidth > container_element.clientWidth;
195
- }
196
- }
197
- onMount(() => {
198
- check_thumbnails_overflow();
199
- document.addEventListener("fullscreenchange", () => {
200
- is_full_screen = !!document.fullscreenElement;
201
- });
202
- window.addEventListener("resize", check_thumbnails_overflow);
203
- return () => window.removeEventListener("resize", check_thumbnails_overflow);
204
- });
205
- $: resolved_value, check_thumbnails_overflow();
206
- $: if (container_element) {
207
- check_thumbnails_overflow();
208
- }
209
- function handle_item_delete(index) {
210
- if (!value || !resolved_value) return;
211
- const deleted_item = resolved_value[index];
212
- let deleted_file_data;
213
- if ("image" in deleted_item) {
214
- deleted_file_data = {
215
- file: deleted_item.image,
216
- index
217
- };
218
- } else if ("video" in deleted_item) {
219
- deleted_file_data = {
220
- file: deleted_item.video,
221
- index
222
- };
223
- }
224
- if (deleted_file_data) {
225
- dispatch("delete", deleted_file_data);
226
- }
227
- }
228
- 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;
229
314
  </script>
230
315
 
231
316
  <svelte:window bind:innerHeight={window_height} />
@@ -238,7 +323,7 @@ let uploading = false;
238
323
  {:else}
239
324
  <div class="gallery-container" bind:this={image_container}>
240
325
  {#if selected_media && allow_preview}
241
- <button
326
+ <span
242
327
  on:keydown={on_keydown}
243
328
  class="preview"
244
329
  class:minimal={mode === "minimal"}
@@ -304,12 +389,14 @@ let uploading = false;
304
389
  >
305
390
  {#if "image" in selected_media}
306
391
  <Image
307
- 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
+ }}
308
398
  src={selected_media.image.url}
309
- alt={selected_media.caption || ""}
310
- title={selected_media.caption || null}
311
- class={selected_media.caption && "with-caption"}
312
- loading="lazy"
399
+ data_testid="detailed-image"
313
400
  />
314
401
  {:else}
315
402
  <Video
@@ -351,10 +438,13 @@ let uploading = false;
351
438
  {#if "image" in media}
352
439
  <Image
353
440
  src={media.image.url}
354
- title={media.caption || null}
355
- data-testid={"thumbnail " + (i + 1)}
356
- alt=""
357
- 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}`}
358
448
  />
359
449
  {:else}
360
450
  <Play />
@@ -371,7 +461,7 @@ let uploading = false;
371
461
  </button>
372
462
  {/each}
373
463
  </div>
374
- </button>
464
+ </span>
375
465
  {/if}
376
466
 
377
467
  <div
@@ -382,10 +472,17 @@ let uploading = false;
382
472
  style:height={height !== "auto" ? height + "px" : null}
383
473
  >
384
474
  {#if interactive && selected_index === null}
385
- <ModifyUpload {i18n} on:clear={() => (value = [])}>
475
+ <ModifyUpload
476
+ {i18n}
477
+ on:clear={() => {
478
+ value = [];
479
+ dispatch("clear");
480
+ }}
481
+ >
386
482
  {#if upload && stream_handler}
387
483
  <IconButton Icon={UploadIcon} label={i18n("common.upload")}>
388
484
  <UploadComponent
485
+ bind:upload_promise
389
486
  icon_upload={true}
390
487
  on:load={(e) => dispatch("upload", e.detail)}
391
488
  filetype={file_types}
@@ -464,7 +561,7 @@ let uploading = false;
464
561
  </div>
465
562
  {/if}
466
563
 
467
- <style>
564
+ <style lang="postcss">
468
565
  .image-container {
469
566
  height: 100%;
470
567
  position: relative;