@gradio/gallery 0.16.1 → 0.17.0

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.
@@ -7,13 +7,12 @@
7
7
  IconButtonWrapper,
8
8
  FullscreenButton
9
9
  } from "@gradio/atoms";
10
- import type { CustomButton as CustomButtonType } from "@gradio/utils";
10
+ import { type CustomButton as CustomButtonType } from "@gradio/utils";
11
11
  import { ModifyUpload, Upload as UploadComponent } from "@gradio/upload";
12
- import type { SelectData } from "@gradio/utils";
13
12
  import { Image } from "@gradio/image/shared";
14
13
  import { Video } from "@gradio/video/shared";
15
14
  import { dequal } from "dequal";
16
- import { createEventDispatcher, onMount } from "svelte";
15
+ import { onMount } from "svelte";
17
16
  import { tick } from "svelte";
18
17
  import type { GalleryImage, GalleryVideo } from "../types";
19
18
 
@@ -22,7 +21,10 @@
22
21
  Image as ImageIcon,
23
22
  Clear,
24
23
  Play,
25
- Upload as UploadIcon
24
+ Upload as UploadIcon,
25
+ Webcam,
26
+ Video as VideoIcon,
27
+ ImagePaste
26
28
  } from "@gradio/icons";
27
29
  import { FileData } from "@gradio/client";
28
30
  import type { Client } from "@gradio/client";
@@ -31,59 +33,93 @@
31
33
 
32
34
  type GalleryData = GalleryImage | GalleryVideo;
33
35
 
34
- export let show_label = true;
35
- export let label: string;
36
- export let value: GalleryData[] | null = null;
37
- export let columns: number | number[] | undefined = [2];
38
- export let rows: number | number[] | undefined = undefined;
39
- export let height: number | "auto" = "auto";
40
- export let preview: boolean;
41
- export let allow_preview = true;
42
- export let object_fit: "contain" | "cover" | "fill" | "none" | "scale-down" =
43
- "cover";
44
- export let show_share_button = false;
45
- export let show_download_button = false;
46
- export let i18n: I18nFormatter;
47
- export let selected_index: number | null = null;
48
- export let interactive: boolean;
49
- export let _fetch: typeof fetch;
50
- export let mode: "normal" | "minimal" = "normal";
51
- export let show_fullscreen_button = true;
52
- export let display_icon_button_wrapper_top_corner = false;
53
- export let fullscreen = false;
54
- export let root = "";
55
- export let file_types: string[] | null = ["image", "video"];
56
- export let max_file_size: number | null = null;
57
- export let upload: Client["upload"] | undefined = undefined;
58
- export let stream_handler: Client["stream"] | undefined = undefined;
59
- export let fit_columns = true;
60
- export let upload_promise: Promise<any> | null = null;
61
- export let buttons: (string | CustomButtonType)[] | null = null;
62
- export let on_custom_button_click: ((id: number) => void) | null = null;
63
-
36
+ let {
37
+ show_label = true,
38
+ label,
39
+ value = $bindable(),
40
+ columns = [2],
41
+ rows = undefined,
42
+ height = "auto",
43
+ preview,
44
+ allow_preview = true,
45
+ object_fit = "cover",
46
+ show_share_button = false,
47
+ show_download_button = false,
48
+ i18n,
49
+ selected_index = $bindable(),
50
+ interactive,
51
+ _fetch,
52
+ show_fullscreen_button = true,
53
+ fullscreen = false,
54
+ root = "",
55
+ file_types = ["image", "video"],
56
+ sources,
57
+ max_file_size = null,
58
+ upload,
59
+ stream_handler,
60
+ fit_columns = true,
61
+ buttons = null,
62
+ oncustom_button_click = null,
63
+ onpreview_open = () => {},
64
+ onchange = () => {},
65
+ onclear = () => {},
66
+ onselect = (data) => {},
67
+ onshare = (data) => {},
68
+ onerror = (error) => {},
69
+ onpreview_close = () => {},
70
+ onfullscreen = (data) => {},
71
+ ondelete = () => {},
72
+ onupload = () => {},
73
+ onsource_change = () => {}
74
+ }: {
75
+ show_label: boolean;
76
+ label: string;
77
+ value: GalleryData[] | null;
78
+ columns: number | number[] | undefined;
79
+ rows: number | number[] | undefined;
80
+ height: number | "auto";
81
+ preview: boolean;
82
+ allow_preview: boolean;
83
+ object_fit: "contain" | "cover" | "fill" | "none" | "scale-down";
84
+ show_share_button: boolean;
85
+ show_download_button: boolean;
86
+ i18n: I18nFormatter;
87
+ selected_index: number | null;
88
+ interactive: boolean;
89
+ _fetch: typeof fetch;
90
+ show_fullscreen_button: boolean;
91
+ fullscreen: boolean;
92
+ root: string;
93
+ file_types: string[] | null;
94
+ sources: ("upload" | "webcam" | "clipboard" | "webcam-video")[];
95
+ max_file_size: number | null;
96
+ upload: Client["upload"] | undefined;
97
+ stream_handler: Client["stream"] | undefined;
98
+ fit_columns: boolean;
99
+ buttons: (string | CustomButtonType)[] | null;
100
+ oncustom_button_click: ((id: number) => void) | null;
101
+ onpreview_open: () => void;
102
+ onchange: () => void;
103
+ onclear: () => void;
104
+ onselect: (data: any) => void;
105
+ onshare: (data: any) => void;
106
+ onerror: (error: any) => void;
107
+ onpreview_close: () => void;
108
+ onfullscreen: (data: any) => void;
109
+ ondelete: (data: any) => void;
110
+ onupload: (data: FileData | FileData[]) => void;
111
+ onsource_change: (source: string) => void;
112
+ } = $props();
113
+
114
+ let upload_promise: Promise<any> | null = null;
115
+ let mode: "normal" | "minimal" = "normal";
116
+ let display_icon_button_wrapper_top_corner = false;
64
117
  let is_full_screen = false;
65
118
  let image_container: HTMLElement;
66
119
 
67
- const dispatch = createEventDispatcher<{
68
- change: undefined;
69
- select: SelectData;
70
- preview_open: undefined;
71
- preview_close: undefined;
72
- fullscreen: boolean;
73
- delete: { file: FileData; index: number };
74
- upload: FileData | FileData[];
75
- error: string;
76
- clear: undefined;
77
- }>();
78
-
79
- // tracks whether the value of the gallery was reset
80
- let was_reset = true;
81
-
82
- $: was_reset = value == null || value.length === 0 ? true : was_reset;
83
-
84
- let resolved_value: GalleryData[] | null = null;
120
+ let was_reset: boolean = $state(false);
85
121
 
86
- $: resolved_value =
122
+ let resolved_value = $derived.by(() =>
87
123
  value == null
88
124
  ? null
89
125
  : (value.map((data) => {
@@ -96,55 +132,68 @@
96
132
  return { image: data.image as FileData, caption: data.caption };
97
133
  }
98
134
  return {};
99
- }) as GalleryData[]);
100
-
101
- let effective_columns: number | number[] | undefined = columns;
102
-
103
- $: {
135
+ }) as GalleryData[])
136
+ );
137
+
138
+ function resolve_effective_columns(
139
+ resolved_value: GalleryData[] | null,
140
+ columns: number | number[] | undefined,
141
+ fit_columns: boolean
142
+ ) {
104
143
  if (resolved_value && columns && fit_columns) {
105
144
  const item_count = resolved_value.length;
106
145
  if (Array.isArray(columns)) {
107
- effective_columns = columns.map((col) => Math.min(col, item_count));
146
+ return columns.map((col) => Math.min(col, item_count));
108
147
  } else {
109
- effective_columns = Math.min(columns, item_count);
148
+ return Math.min(columns, item_count);
110
149
  }
111
150
  } else {
112
- effective_columns = columns;
151
+ return columns;
113
152
  }
114
153
  }
115
154
 
116
- let prev_value: GalleryData[] | null = value;
117
- if (selected_index == null && preview && value?.length) {
118
- selected_index = 0;
119
- }
120
- let old_selected_index: number | null = selected_index;
121
-
122
- $: if (!dequal(prev_value, value)) {
123
- // When value is falsy (clear button or first load),
124
- // preview determines the selected image
125
- if (was_reset) {
126
- selected_index = preview && value?.length ? 0 : null;
127
- was_reset = false;
128
- // Otherwise we keep the selected_index the same if the
129
- // gallery has at least as many elements as it did before
130
- } else {
131
- if (selected_index !== null && value !== null) {
132
- selected_index = Math.max(
133
- 0,
134
- Math.min(selected_index, value.length - 1)
135
- );
155
+ let effective_columns: number | number[] | undefined = $derived.by(() =>
156
+ resolve_effective_columns(resolved_value, columns, fit_columns)
157
+ );
158
+
159
+ let prev_value: GalleryData[] | null = $state(value);
160
+ // if (selected_index == null && preview && value?.length) {
161
+ // selected_index = 0;
162
+ // }
163
+ let old_selected_index: number | null = $state(selected_index);
164
+
165
+ $effect(() => {
166
+ if (!dequal(prev_value, value)) {
167
+ // When value is falsy (clear button or first load),
168
+ // preview determines the selected image
169
+ if (was_reset) {
170
+ selected_index = preview && value?.length ? 0 : null;
171
+ was_reset = false;
172
+ // Otherwise we keep the selected_index the same if the
173
+ // gallery has at least as many elements as it did before
136
174
  } else {
137
- selected_index = null;
175
+ if (selected_index !== null && value !== null) {
176
+ selected_index = Math.max(
177
+ 0,
178
+ Math.min(selected_index, value.length - 1)
179
+ );
180
+ } else {
181
+ selected_index = null;
182
+ }
138
183
  }
184
+ onchange();
185
+ prev_value = value;
139
186
  }
140
- dispatch("change");
141
- prev_value = value;
142
- }
187
+ });
143
188
 
144
- $: previous =
145
- ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) %
146
- (resolved_value?.length ?? 0);
147
- $: next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
189
+ let previous = $derived.by(
190
+ () =>
191
+ ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) %
192
+ (resolved_value?.length ?? 0)
193
+ );
194
+ let next = $derived.by(
195
+ () => ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0)
196
+ );
148
197
 
149
198
  function handle_preview_click(event: MouseEvent): void {
150
199
  const element = event.target as HTMLElement;
@@ -164,7 +213,7 @@
164
213
  case "Escape":
165
214
  e.preventDefault();
166
215
  selected_index = null;
167
- dispatch("preview_close");
216
+ onpreview_close();
168
217
  break;
169
218
  case "ArrowLeft":
170
219
  e.preventDefault();
@@ -179,7 +228,7 @@
179
228
  }
180
229
  }
181
230
 
182
- $: {
231
+ $effect(() => {
183
232
  if (selected_index !== old_selected_index) {
184
233
  old_selected_index = selected_index;
185
234
  if (selected_index !== null) {
@@ -189,26 +238,29 @@
189
238
  Math.min(selected_index, resolved_value.length - 1)
190
239
  );
191
240
  }
192
- dispatch("select", {
241
+ onselect({
193
242
  index: selected_index,
194
243
  value: resolved_value?.[selected_index]
195
244
  });
196
245
  }
197
246
  }
198
- }
247
+ });
199
248
 
200
- $: if (allow_preview) {
201
- scroll_to_img(selected_index);
202
- }
249
+ $effect(() => {
250
+ if (allow_preview && container_element) {
251
+ scroll_to_img(selected_index);
252
+ }
253
+ });
203
254
 
204
255
  let el: HTMLButtonElement[] = [];
205
- let container_element: HTMLDivElement;
256
+ let container_element: HTMLDivElement | undefined = $state();
206
257
 
207
258
  async function scroll_to_img(index: number | null): Promise<void> {
208
259
  if (typeof index !== "number") return;
209
260
  await tick();
210
261
 
211
262
  if (el[index] === undefined) return;
263
+ if (!container_element) return;
212
264
 
213
265
  el[index]?.focus();
214
266
 
@@ -262,10 +314,11 @@
262
314
  URL.revokeObjectURL(url);
263
315
  }
264
316
 
265
- $: selected_media =
317
+ let selected_media = $derived.by(() =>
266
318
  selected_index != null && resolved_value != null
267
319
  ? resolved_value[selected_index]
268
- : null;
320
+ : null
321
+ );
269
322
 
270
323
  let thumbnails_overflow = false;
271
324
 
@@ -286,10 +339,13 @@
286
339
  window.removeEventListener("resize", check_thumbnails_overflow);
287
340
  });
288
341
 
289
- $: (resolved_value, check_thumbnails_overflow());
290
- $: if (container_element) {
342
+ $effect(() => {
343
+ resolved_value;
291
344
  check_thumbnails_overflow();
292
- }
345
+ if (container_element) {
346
+ check_thumbnails_overflow();
347
+ }
348
+ });
293
349
 
294
350
  function handle_item_delete(index: number): void {
295
351
  if (!value || !resolved_value) return;
@@ -310,7 +366,7 @@
310
366
  }
311
367
 
312
368
  if (deleted_file_data) {
313
- dispatch("delete", deleted_file_data);
369
+ ondelete(deleted_file_data);
314
370
  }
315
371
  }
316
372
 
@@ -335,13 +391,13 @@
335
391
  <IconButtonWrapper
336
392
  display_top_corner={display_icon_button_wrapper_top_corner}
337
393
  {buttons}
338
- {on_custom_button_click}
394
+ on_custom_button_click={oncustom_button_click}
339
395
  >
340
396
  {#if show_download_button}
341
397
  <IconButton
342
398
  Icon={Download}
343
399
  label={i18n("common.download")}
344
- on:click={() => {
400
+ onclick={() => {
345
401
  const image =
346
402
  "image" in selected_media
347
403
  ? selected_media?.image
@@ -358,15 +414,19 @@
358
414
  {/if}
359
415
 
360
416
  {#if show_fullscreen_button}
361
- <FullscreenButton {fullscreen} on:fullscreen />
417
+ <FullscreenButton {fullscreen} onclick={(e) => onfullscreen(e)} />
362
418
  {/if}
363
419
 
364
420
  {#if show_share_button}
365
421
  <div class="icon-button">
366
422
  <ShareButton
367
423
  {i18n}
368
- on:share
369
- on:error
424
+ on:share={(detail) => {
425
+ onshare(detail);
426
+ }}
427
+ on:error={(detail) => {
428
+ onerror(detail);
429
+ }}
370
430
  value={resolved_value}
371
431
  formatter={format_gallery_for_sharing}
372
432
  />
@@ -376,9 +436,9 @@
376
436
  <IconButton
377
437
  Icon={Clear}
378
438
  label="Close"
379
- on:click={() => {
439
+ onclick={() => {
380
440
  selected_index = null;
381
- dispatch("preview_close");
441
+ onpreview_close();
382
442
  }}
383
443
  />
384
444
  {/if}
@@ -480,28 +540,59 @@
480
540
  {#if interactive && selected_index === null}
481
541
  <ModifyUpload
482
542
  {i18n}
483
- on:clear={() => {
543
+ onclear={() => {
484
544
  value = [];
485
- dispatch("clear");
545
+ onsource_change("upload");
546
+ onclear();
486
547
  }}
487
548
  >
488
549
  {#if upload && stream_handler}
489
- <IconButton Icon={UploadIcon} label={i18n("common.upload")}>
550
+ <IconButton
551
+ Icon={UploadIcon}
552
+ label={i18n("upload_text.click_to_upload")}
553
+ >
490
554
  <UploadComponent
491
555
  bind:upload_promise
492
556
  icon_upload={true}
493
- on:load={(e) => dispatch("upload", e.detail)}
557
+ onload={(e) => onupload(e)}
494
558
  filetype={file_types}
495
559
  file_count="multiple"
496
560
  {max_file_size}
497
561
  {root}
498
562
  bind:uploading
499
- on:error={(e) => dispatch("error", e.detail)}
563
+ onerror={(e) => onerror(e)}
500
564
  {stream_handler}
501
565
  {upload}
502
566
  />
503
567
  </IconButton>
504
568
  {/if}
569
+ {#if sources.includes("webcam")}
570
+ <IconButton
571
+ Icon={Webcam}
572
+ label={i18n("common.webcam")}
573
+ onclick={() => {
574
+ onsource_change("webcam");
575
+ }}
576
+ />
577
+ {/if}
578
+ {#if sources.includes("webcam-video")}
579
+ <IconButton
580
+ Icon={VideoIcon}
581
+ label={i18n("common.video")}
582
+ onclick={() => {
583
+ onsource_change("webcam-video");
584
+ }}
585
+ />
586
+ {/if}
587
+ {#if sources.includes("clipboard")}
588
+ <IconButton
589
+ label={i18n("upload_text.paste_clipboard")}
590
+ onclick={() => {
591
+ onsource_change("clipboard");
592
+ }}
593
+ Icon={ImagePaste}
594
+ />
595
+ {/if}
505
596
  </ModifyUpload>
506
597
  {/if}
507
598
  <div
@@ -516,7 +607,7 @@
516
607
  class:selected={selected_index === i}
517
608
  on:click={() => {
518
609
  if (selected_index === null && allow_preview) {
519
- dispatch("preview_open");
610
+ onpreview_open();
520
611
  }
521
612
  selected_index = i;
522
613
  }}
@@ -1,67 +1,48 @@
1
- import type { CustomButton as CustomButtonType } from "@gradio/utils";
2
- import type { SelectData } from "@gradio/utils";
1
+ import { type CustomButton as CustomButtonType } from "@gradio/utils";
3
2
  import type { GalleryImage, GalleryVideo } from "../types";
4
3
  import { FileData } from "@gradio/client";
5
4
  import type { Client } from "@gradio/client";
6
5
  import type { I18nFormatter } from "@gradio/utils";
7
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
8
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
9
- $$bindings?: Bindings;
10
- } & Exports;
11
- (internal: unknown, props: Props & {
12
- $$events?: Events;
13
- $$slots?: Slots;
14
- }): Exports & {
15
- $set?: any;
16
- $on?: any;
17
- };
18
- z_$$bindings?: Bindings;
19
- }
20
- declare const Gallery: $$__sveltets_2_IsomorphicComponent<{
21
- show_label?: boolean;
6
+ type GalleryData = GalleryImage | GalleryVideo;
7
+ type $$ComponentProps = {
8
+ show_label: boolean;
22
9
  label: string;
23
- value?: (GalleryImage | GalleryVideo)[] | null;
24
- columns?: number | number[] | undefined;
25
- rows?: number | number[] | undefined;
26
- height?: number | "auto";
10
+ value: GalleryData[] | null;
11
+ columns: number | number[] | undefined;
12
+ rows: number | number[] | undefined;
13
+ height: number | "auto";
27
14
  preview: boolean;
28
- allow_preview?: boolean;
29
- object_fit?: "contain" | "cover" | "fill" | "none" | "scale-down";
30
- show_share_button?: boolean;
31
- show_download_button?: boolean;
15
+ allow_preview: boolean;
16
+ object_fit: "contain" | "cover" | "fill" | "none" | "scale-down";
17
+ show_share_button: boolean;
18
+ show_download_button: boolean;
32
19
  i18n: I18nFormatter;
33
- selected_index?: number | null;
20
+ selected_index: number | null;
34
21
  interactive: boolean;
35
22
  _fetch: typeof fetch;
36
- mode?: "normal" | "minimal";
37
- show_fullscreen_button?: boolean;
38
- display_icon_button_wrapper_top_corner?: boolean;
39
- fullscreen?: boolean;
40
- root?: string;
41
- file_types?: string[] | null;
42
- max_file_size?: number | null;
43
- upload?: Client["upload"] | undefined;
44
- stream_handler?: Client["stream"] | undefined;
45
- fit_columns?: boolean;
46
- upload_promise?: Promise<any> | null;
47
- buttons?: (string | CustomButtonType)[] | null;
48
- on_custom_button_click?: ((id: number) => void) | null;
49
- }, {
50
- fullscreen: CustomEvent<any>;
51
- share: CustomEvent<import("@gradio/utils").ShareData>;
52
- error: CustomEvent<any>;
53
- change: CustomEvent<undefined>;
54
- select: CustomEvent<SelectData>;
55
- preview_open: CustomEvent<undefined>;
56
- preview_close: CustomEvent<undefined>;
57
- delete: CustomEvent<{
58
- file: FileData;
59
- index: number;
60
- }>;
61
- upload: CustomEvent<FileData | FileData[]>;
62
- clear: CustomEvent<undefined>;
63
- } & {
64
- [evt: string]: CustomEvent<any>;
65
- }, {}, {}, string>;
66
- type Gallery = InstanceType<typeof Gallery>;
23
+ show_fullscreen_button: boolean;
24
+ fullscreen: boolean;
25
+ root: string;
26
+ file_types: string[] | null;
27
+ sources: ("upload" | "webcam" | "clipboard" | "webcam-video")[];
28
+ max_file_size: number | null;
29
+ upload: Client["upload"] | undefined;
30
+ stream_handler: Client["stream"] | undefined;
31
+ fit_columns: boolean;
32
+ buttons: (string | CustomButtonType)[] | null;
33
+ oncustom_button_click: ((id: number) => void) | null;
34
+ onpreview_open: () => void;
35
+ onchange: () => void;
36
+ onclear: () => void;
37
+ onselect: (data: any) => void;
38
+ onshare: (data: any) => void;
39
+ onerror: (error: any) => void;
40
+ onpreview_close: () => void;
41
+ onfullscreen: (data: any) => void;
42
+ ondelete: (data: any) => void;
43
+ onupload: (data: FileData | FileData[]) => void;
44
+ onsource_change: (source: string) => void;
45
+ };
46
+ declare const Gallery: import("svelte").Component<$$ComponentProps, {}, "value" | "selected_index">;
47
+ type Gallery = ReturnType<typeof Gallery>;
67
48
  export default Gallery;
@@ -1,2 +1,3 @@
1
- import type { FileData } from "@gradio/client";
1
+ import { FileData, Client } from "@gradio/client";
2
2
  export declare function format_gallery_for_sharing(value: [FileData, string | null][] | null): Promise<string>;
3
+ export declare function handle_save(img_blob: Blob, upload: Client["upload"], filename?: string): Promise<FileData[]>;
@@ -1,4 +1,5 @@
1
1
  import { uploadToHuggingFace } from "@gradio/utils";
2
+ import { FileData, Client } from "@gradio/client";
2
3
  export async function format_gallery_for_sharing(value) {
3
4
  if (!value)
4
5
  return "";
@@ -11,3 +12,18 @@ export async function format_gallery_for_sharing(value) {
11
12
  .map((url) => `<img src="${url}" style="height: 400px" />`)
12
13
  .join("")}</div>`;
13
14
  }
15
+ export async function handle_save(img_blob, upload, filename = "uploaded_file") {
16
+ const ext = img_blob.type.split("/")[1] || "png";
17
+ const f_ = new File([img_blob], `${filename}.${ext}`);
18
+ const files = [
19
+ new FileData({
20
+ path: f_.name,
21
+ orig_name: f_.name,
22
+ blob: f_,
23
+ size: f_.size,
24
+ mime_type: f_.type,
25
+ is_stream: false
26
+ })
27
+ ];
28
+ return (await upload(...files));
29
+ }
package/dist/types.d.ts CHANGED
@@ -22,6 +22,7 @@ export interface GalleryProps {
22
22
  buttons: (string | CustomButton)[];
23
23
  type: "numpy" | "pil" | "filepath";
24
24
  fit_columns: boolean;
25
+ sources: ("upload" | "webcam-video" | "webcam" | "clipboard")[];
25
26
  }
26
27
  export interface GalleryEvents {
27
28
  change: GalleryData[] | null;
@@ -39,4 +40,5 @@ export interface GalleryEvents {
39
40
  custom_button_click: {
40
41
  id: number;
41
42
  };
43
+ warning: string;
42
44
  }