@gradio/gallery 0.17.8 → 0.19.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @gradio/gallery
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Features
6
+
7
+ - [#13566](https://github.com/gradio-app/gradio/pull/13566) [`79b9171`](https://github.com/gradio-app/gradio/commit/79b91718b5c972c174ed042d8b04443bc2274076) - Gallery Download All. Thanks @dawoodkhan82!
8
+ - [#13543](https://github.com/gradio-app/gradio/pull/13543) [`0533483`](https://github.com/gradio-app/gradio/commit/0533483bccdee38f334a598f18297e8c02966343) - Migrate Image components to Svelte 5. Thanks @dawoodkhan82!
9
+
10
+ ### Dependency updates
11
+
12
+ - @gradio/statustracker@0.15.1
13
+ - @gradio/icons@0.16.0
14
+ - @gradio/atoms@0.26.0
15
+ - @gradio/image@0.28.0
16
+ - @gradio/video@0.22.0
17
+ - @gradio/client@2.3.1
18
+ - @gradio/upload@0.18.1
19
+ - @gradio/file@0.15.1
20
+
21
+ ## 0.18.0
22
+
23
+ ### Features
24
+
25
+ - [#13526](https://github.com/gradio-app/gradio/pull/13526) [`53cb4ca`](https://github.com/gradio-app/gradio/commit/53cb4cae1ec3521e9170d12867253516413ba37a) - Run `pnpm lint` and `pnpm ts:check` on CI. Thanks @abidlabs!
26
+
27
+ ### Dependency updates
28
+
29
+ - @gradio/atoms@0.25.0
30
+ - @gradio/statustracker@0.15.0
31
+ - @gradio/utils@0.13.0
32
+ - @gradio/client@2.3.0
33
+ - @gradio/upload@0.18.0
34
+ - @gradio/image@0.27.0
35
+ - @gradio/file@0.15.0
36
+ - @gradio/video@0.21.0
37
+
38
+ ## 0.17.8
39
+
40
+ ### Dependency updates
41
+
42
+ - @gradio/client@2.2.2
43
+
44
+ ## 0.17.8
45
+
46
+ ### Dependency updates
47
+
48
+ - @gradio/client@2.2.1
49
+ - @gradio/upload@0.17.10
50
+
3
51
  ## 0.17.8
4
52
 
5
53
  ### Fixes
@@ -2,8 +2,6 @@
2
2
  import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import Gallery from "./Index.svelte";
4
4
  import { allModes } from "../storybook/modes";
5
- import { within } from "@testing-library/dom";
6
- import { userEvent } from "storybook/test";
7
5
  import { wrapProps } from "../storybook/wrapProps";
8
6
 
9
7
  const cheetah = "/cheetah.jpg";
@@ -157,15 +155,6 @@
157
155
  buttons: ["fullscreen", "download", "share"],
158
156
  sources: ["upload"]
159
157
  }}
160
- play={async ({ canvasElement }) => {
161
- const canvas = within(canvasElement);
162
- const image = await canvas.findByLabelText("Thumbnail 1 of 8");
163
- await userEvent.click(image);
164
- const expand_btn = await canvas.findByRole("button", {
165
- name: "Fullscreen"
166
- });
167
- await userEvent.click(expand_btn);
168
- }}
169
158
  >
170
159
  {#snippet template(args)}
171
160
  <Gallery {...wrapProps({ value: galleryValue, ...args })} />
@@ -186,37 +175,6 @@
186
175
  {/snippet}
187
176
  </Story>
188
177
 
189
- <Story
190
- name="Gallery with rows=3 and columns=3"
191
- args={{
192
- label: "My Cheetah Gallery",
193
- show_label: true,
194
- rows: 3,
195
- columns: 3,
196
- buttons: [],
197
- sources: ["upload"]
198
- }}
199
- >
200
- {#snippet template(args)}
201
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
202
- {/snippet}
203
- </Story>
204
-
205
- <Story
206
- name="Gallery with columns=4"
207
- args={{
208
- label: "My Cheetah Gallery",
209
- show_label: true,
210
- columns: 4,
211
- buttons: [],
212
- sources: ["upload"]
213
- }}
214
- >
215
- {#snippet template(args)}
216
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
217
- {/snippet}
218
- </Story>
219
-
220
178
  <Story
221
179
  name="Gallery with height=600"
222
180
  args={{
@@ -231,21 +189,6 @@
231
189
  {/snippet}
232
190
  </Story>
233
191
 
234
- <Story
235
- name="Gallery with allow_preview=false"
236
- args={{
237
- label: "My Cheetah Gallery",
238
- show_label: true,
239
- allow_preview: false,
240
- buttons: [],
241
- sources: ["upload"]
242
- }}
243
- >
244
- {#snippet template(args)}
245
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
246
- {/snippet}
247
- </Story>
248
-
249
192
  <Story
250
193
  name="Gallery with preview"
251
194
  args={{
@@ -261,36 +204,6 @@
261
204
  {/snippet}
262
205
  </Story>
263
206
 
264
- <Story
265
- name="Gallery with object_fit=scale-down"
266
- args={{
267
- label: "My Cheetah Gallery",
268
- show_label: true,
269
- object_fit: "scale-down",
270
- buttons: [],
271
- sources: ["upload"]
272
- }}
273
- >
274
- {#snippet template(args)}
275
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
276
- {/snippet}
277
- </Story>
278
-
279
- <Story
280
- name="Gallery with object_fit=contain"
281
- args={{
282
- label: "My Cheetah Gallery",
283
- show_label: true,
284
- object_fit: "contain",
285
- buttons: [],
286
- sources: ["upload"]
287
- }}
288
- >
289
- {#snippet template(args)}
290
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
291
- {/snippet}
292
- </Story>
293
-
294
207
  <Story
295
208
  name="Gallery with object_fit=cover"
296
209
  args={{
@@ -306,21 +219,6 @@
306
219
  {/snippet}
307
220
  </Story>
308
221
 
309
- <Story
310
- name="Gallery with object_fit=none"
311
- args={{
312
- label: "My Cheetah Gallery",
313
- show_label: true,
314
- object_fit: "none",
315
- buttons: [],
316
- sources: ["upload"]
317
- }}
318
- >
319
- {#snippet template(args)}
320
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
321
- {/snippet}
322
- </Story>
323
-
324
222
  <Story
325
223
  name="Gallery with object_fit=fill"
326
224
  args={{
@@ -336,69 +234,6 @@
336
234
  {/snippet}
337
235
  </Story>
338
236
 
339
- <Story
340
- name="Gallery with share button"
341
- args={{
342
- label: "My Cheetah Gallery",
343
- show_label: true,
344
- buttons: ["share"],
345
- sources: ["upload"]
346
- }}
347
- >
348
- {#snippet template(args)}
349
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
350
- {/snippet}
351
- </Story>
352
-
353
- <Story
354
- name="Gallery with overflow of images"
355
- args={{
356
- label: "My Cheetah Gallery",
357
- show_label: true,
358
- rows: 2,
359
- columns: 2,
360
- height: 400,
361
- buttons: [],
362
- sources: ["upload"]
363
- }}
364
- >
365
- {#snippet template(args)}
366
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
367
- {/snippet}
368
- </Story>
369
-
370
- <Story
371
- name="Gallery with overflow of images and short height"
372
- args={{
373
- label: "My Cheetah Gallery",
374
- show_label: true,
375
- rows: 2,
376
- columns: 2,
377
- height: 200,
378
- buttons: [],
379
- sources: ["upload"]
380
- }}
381
- >
382
- {#snippet template(args)}
383
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
384
- {/snippet}
385
- </Story>
386
-
387
- <Story
388
- name="Gallery with download button"
389
- args={{
390
- label: "My Cheetah Gallery",
391
- rows: 2,
392
- height: 400,
393
- buttons: ["download"],
394
- sources: ["upload"]
395
- }}
396
- >
397
- {#snippet template(args)}
398
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
399
- {/snippet}
400
- </Story>
401
-
402
237
  <Story
403
238
  name="Gallery with fit_columns false"
404
239
  args={{
@@ -413,18 +248,3 @@
413
248
  <Gallery {...wrapProps({ value: galleryValue, ...args })} />
414
249
  {/snippet}
415
250
  </Story>
416
-
417
- <Story
418
- name="Gallery with fit_columns true"
419
- args={{
420
- label: "My Cheetah Gallery",
421
- columns: 10,
422
- buttons: ["download"],
423
- fit_columns: true,
424
- sources: ["upload"]
425
- }}
426
- >
427
- {#snippet template(args)}
428
- <Gallery {...wrapProps({ value: galleryValue, ...args })} />
429
- {/snippet}
430
- </Story>
package/Gallery.test.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { test, describe, afterEach, expect, vi } from "vitest";
2
- import { cleanup, render, fireEvent, waitFor } from "@self/tootils/render";
2
+ import {
3
+ cleanup,
4
+ render,
5
+ fireEvent,
6
+ waitFor,
7
+ upload_file,
8
+ mock_client,
9
+ TEST_JPG
10
+ } from "@self/tootils/render";
3
11
  import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
4
12
 
5
13
  import Gallery from "./Index.svelte";
@@ -376,6 +384,84 @@ describe("Props: buttons", () => {
376
384
  expect(getByRole("button", { name: "common.download" })).toBeVisible();
377
385
  });
378
386
 
387
+ test("buttons=['download_all'] downloads gallery files as a zip from grid view", async () => {
388
+ const fetch = vi.fn(async () => new Response(new Blob(["file"])));
389
+ const blobs: Blob[] = [];
390
+ const clicked_names: string[] = [];
391
+ const create_object_url = vi
392
+ .spyOn(URL, "createObjectURL")
393
+ .mockImplementation((object: Blob | MediaSource) => {
394
+ blobs.push(object as Blob);
395
+ return "blob:gallery";
396
+ });
397
+ const revoke_object_url = vi
398
+ .spyOn(URL, "revokeObjectURL")
399
+ .mockImplementation(() => {});
400
+ const click = vi
401
+ .spyOn(HTMLAnchorElement.prototype, "click")
402
+ .mockImplementation(function (this: HTMLAnchorElement) {
403
+ clicked_names.push(this.download);
404
+ });
405
+
406
+ try {
407
+ const value = [img("cat"), vid("clip"), img("dog")];
408
+ const { queryByRole } = await render(Gallery, {
409
+ ...preview_props,
410
+ value,
411
+ buttons: ["download_all"],
412
+ client: { fetch }
413
+ });
414
+
415
+ expect(
416
+ queryByRole("button", { name: "common.download_all" })
417
+ ).not.toBeInTheDocument();
418
+
419
+ cleanup();
420
+
421
+ const { getByRole } = await render(Gallery, {
422
+ ...default_props,
423
+ value,
424
+ buttons: ["download_all"],
425
+ client: { fetch }
426
+ });
427
+
428
+ await fireEvent.click(
429
+ getByRole("button", { name: "common.download_all" })
430
+ );
431
+
432
+ await waitFor(() => {
433
+ expect(fetch).toHaveBeenCalledTimes(3);
434
+ expect(click).toHaveBeenCalledTimes(1);
435
+ });
436
+
437
+ expect(fetch).toHaveBeenNthCalledWith(1, "https://example.com/cat.png");
438
+ expect(fetch).toHaveBeenNthCalledWith(2, "https://example.com/clip.mp4");
439
+ expect(fetch).toHaveBeenNthCalledWith(3, "https://example.com/dog.png");
440
+ expect(create_object_url).toHaveBeenCalledTimes(1);
441
+ expect(revoke_object_url).toHaveBeenCalledWith("blob:gallery");
442
+ expect(clicked_names).toEqual(["gallery.zip"]);
443
+ expect(blobs[0].type).toBe("application/zip");
444
+
445
+ const zip = new Uint8Array(await blobs[0].arrayBuffer());
446
+ expect(Array.from(zip.slice(0, 4))).toEqual([0x50, 0x4b, 0x03, 0x04]);
447
+ const zip_view = new DataView(zip.buffer);
448
+ expect(zip_view.getUint16(6, true)).toBe(0x0800);
449
+ const central_header_index = zip.findIndex((_, index) =>
450
+ [0x50, 0x4b, 0x01, 0x02].every(
451
+ (byte, offset) => zip[index + offset] === byte
452
+ )
453
+ );
454
+ expect(central_header_index).toBeGreaterThan(-1);
455
+ expect(zip_view.getUint16(central_header_index + 8, true)).toBe(0x0800);
456
+ const zip_text = new TextDecoder().decode(zip);
457
+ expect(zip_text).toContain("cat.png");
458
+ expect(zip_text).toContain("clip.mp4");
459
+ expect(zip_text).toContain("dog.png");
460
+ } finally {
461
+ vi.restoreAllMocks();
462
+ }
463
+ });
464
+
379
465
  test("buttons=['fullscreen'] shows fullscreen button in preview", async () => {
380
466
  const { getByLabelText } = await render(Gallery, {
381
467
  ...preview_props,
@@ -610,6 +696,30 @@ describe("Events: change", () => {
610
696
  });
611
697
  });
612
698
 
699
+ describe("Events: upload", () => {
700
+ afterEach(() => cleanup());
701
+
702
+ test("upload: file input dispatches upload and change events", async () => {
703
+ const { listen } = await render(Gallery, {
704
+ ...default_props,
705
+ interactive: true,
706
+ value: null,
707
+ root: "https://example.com",
708
+ client: mock_client()
709
+ });
710
+
711
+ const upload = listen("upload");
712
+ const change = listen("change");
713
+
714
+ await upload_file(TEST_JPG);
715
+
716
+ await waitFor(() => {
717
+ expect(upload).toHaveBeenCalledTimes(1);
718
+ });
719
+ expect(change).toHaveBeenCalledTimes(1);
720
+ });
721
+ });
722
+
613
723
  describe("Events: select", () => {
614
724
  afterEach(() => cleanup());
615
725
 
package/Index.svelte CHANGED
@@ -30,7 +30,7 @@
30
30
  }
31
31
 
32
32
  const props = $props();
33
- const gradio = new GalleryGradio<GalleryEvents, GalleryProps>(props, {
33
+ const gradio = new GalleryGradio(props, {
34
34
  selected_index: null,
35
35
  file_types: ["image", "video"]
36
36
  });
@@ -192,7 +192,7 @@
192
192
  gradio.dispatch("upload", gradio.props.value);
193
193
  gradio.dispatch("change", gradio.props.value);
194
194
  }}
195
- onerror={({ detail }) => {
195
+ onerror={(detail) => {
196
196
  gradio.shared.loading_status = gradio.shared.loading_status || {};
197
197
  gradio.shared.loading_status.status = "error";
198
198
  gradio.dispatch("error", detail);
@@ -205,9 +205,10 @@
205
205
  <Webcam
206
206
  root={gradio.shared.root}
207
207
  value={null}
208
- on:capture={async (e) => {
208
+ oncapture={async (detail) => {
209
+ if (!(detail instanceof Blob)) return;
209
210
  const f = await handle_save(
210
- e.detail,
211
+ detail,
211
212
  (f) => gradio.shared.client.upload(f, gradio.shared.root),
212
213
  "webcam_upload"
213
214
  );
@@ -216,9 +217,6 @@
216
217
  active_source = null;
217
218
  gradio.dispatch("change", gradio.props.value);
218
219
  }}
219
- on:error
220
- on:drag
221
- on:close_stream
222
220
  mirror_webcam={true}
223
221
  streaming={false}
224
222
  mode="image"
@@ -230,17 +228,15 @@
230
228
  <Webcam
231
229
  root={gradio.shared.root}
232
230
  value={null}
233
- on:capture={async (e) => {
234
- const f = { ...e.detail };
231
+ oncapture={async (detail) => {
232
+ if (!detail) return;
233
+ const f = { ...(detail as FileData) };
235
234
  f.mime_type = "video/webm";
236
235
  const processed_files = await process_upload_files([f]);
237
236
  gradio.props.value?.push(...processed_files);
238
237
  active_source = null;
239
238
  gradio.dispatch("change", gradio.props.value);
240
239
  }}
241
- on:error
242
- on:drag
243
- on:close_stream
244
240
  mirror_webcam={true}
245
241
  streaming={false}
246
242
  mode="video"
@@ -254,7 +250,7 @@
254
250
  {sources}
255
251
  bind:active_source
256
252
  handle_clear={() => gradio.dispatch("clear")}
257
- handle_select={handle_select_source}
253
+ handle_select={(source) => handle_select_source(source as any)}
258
254
  />
259
255
  {/if}
260
256
  {:else}
@@ -282,7 +278,7 @@
282
278
  gradio.dispatch("change", gradio.props.value);
283
279
  }}
284
280
  {sources}
285
- {onsource_change}
281
+ onsource_change={(source) => onsource_change(source as any)}
286
282
  label={gradio.shared.label}
287
283
  show_label={gradio.shared.show_label}
288
284
  columns={gradio.props.columns}
@@ -300,6 +296,9 @@
300
296
  show_download_button={gradio.props.buttons.some(
301
297
  (btn) => typeof btn === "string" && btn === "download"
302
298
  )}
299
+ show_download_all_button={gradio.props.buttons.some(
300
+ (btn) => typeof btn === "string" && btn === "download_all"
301
+ )}
303
302
  fit_columns={gradio.props.fit_columns}
304
303
  i18n={gradio.i18n}
305
304
  _fetch={(...args) => gradio.shared.client.fetch(...args)}
package/dist/Index.svelte CHANGED
@@ -30,7 +30,7 @@
30
30
  }
31
31
 
32
32
  const props = $props();
33
- const gradio = new GalleryGradio<GalleryEvents, GalleryProps>(props, {
33
+ const gradio = new GalleryGradio(props, {
34
34
  selected_index: null,
35
35
  file_types: ["image", "video"]
36
36
  });
@@ -192,7 +192,7 @@
192
192
  gradio.dispatch("upload", gradio.props.value);
193
193
  gradio.dispatch("change", gradio.props.value);
194
194
  }}
195
- onerror={({ detail }) => {
195
+ onerror={(detail) => {
196
196
  gradio.shared.loading_status = gradio.shared.loading_status || {};
197
197
  gradio.shared.loading_status.status = "error";
198
198
  gradio.dispatch("error", detail);
@@ -205,9 +205,10 @@
205
205
  <Webcam
206
206
  root={gradio.shared.root}
207
207
  value={null}
208
- on:capture={async (e) => {
208
+ oncapture={async (detail) => {
209
+ if (!(detail instanceof Blob)) return;
209
210
  const f = await handle_save(
210
- e.detail,
211
+ detail,
211
212
  (f) => gradio.shared.client.upload(f, gradio.shared.root),
212
213
  "webcam_upload"
213
214
  );
@@ -216,9 +217,6 @@
216
217
  active_source = null;
217
218
  gradio.dispatch("change", gradio.props.value);
218
219
  }}
219
- on:error
220
- on:drag
221
- on:close_stream
222
220
  mirror_webcam={true}
223
221
  streaming={false}
224
222
  mode="image"
@@ -230,17 +228,15 @@
230
228
  <Webcam
231
229
  root={gradio.shared.root}
232
230
  value={null}
233
- on:capture={async (e) => {
234
- const f = { ...e.detail };
231
+ oncapture={async (detail) => {
232
+ if (!detail) return;
233
+ const f = { ...(detail as FileData) };
235
234
  f.mime_type = "video/webm";
236
235
  const processed_files = await process_upload_files([f]);
237
236
  gradio.props.value?.push(...processed_files);
238
237
  active_source = null;
239
238
  gradio.dispatch("change", gradio.props.value);
240
239
  }}
241
- on:error
242
- on:drag
243
- on:close_stream
244
240
  mirror_webcam={true}
245
241
  streaming={false}
246
242
  mode="video"
@@ -254,7 +250,7 @@
254
250
  {sources}
255
251
  bind:active_source
256
252
  handle_clear={() => gradio.dispatch("clear")}
257
- handle_select={handle_select_source}
253
+ handle_select={(source) => handle_select_source(source as any)}
258
254
  />
259
255
  {/if}
260
256
  {:else}
@@ -282,7 +278,7 @@
282
278
  gradio.dispatch("change", gradio.props.value);
283
279
  }}
284
280
  {sources}
285
- {onsource_change}
281
+ onsource_change={(source) => onsource_change(source as any)}
286
282
  label={gradio.shared.label}
287
283
  show_label={gradio.shared.show_label}
288
284
  columns={gradio.props.columns}
@@ -300,6 +296,9 @@
300
296
  show_download_button={gradio.props.buttons.some(
301
297
  (btn) => typeof btn === "string" && btn === "download"
302
298
  )}
299
+ show_download_all_button={gradio.props.buttons.some(
300
+ (btn) => typeof btn === "string" && btn === "download_all"
301
+ )}
303
302
  fit_columns={gradio.props.fit_columns}
304
303
  i18n={gradio.i18n}
305
304
  _fetch={(...args) => gradio.shared.client.fetch(...args)}
@@ -1,24 +1,5 @@
1
1
  export { default as BaseGallery } from "./shared/Gallery.svelte";
2
2
  export { default as BaseExample } from "./Example.svelte";
3
- 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> {
4
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
5
- $$bindings?: Bindings;
6
- } & Exports;
7
- (internal: unknown, props: Props & {
8
- $$events?: Events;
9
- $$slots?: Slots;
10
- }): Exports & {
11
- $set?: any;
12
- $on?: any;
13
- };
14
- z_$$bindings?: Bindings;
15
- }
16
- declare const Index: $$__sveltets_2_IsomorphicComponent<$$ComponentProps, {
17
- error: CustomEvent<string>;
18
- drag: CustomEvent<any>;
19
- close_stream: CustomEvent<undefined>;
20
- } & {
21
- [evt: string]: CustomEvent<any>;
22
- }, {}, {}, "">;
23
- type Index = InstanceType<typeof Index>;
3
+ declare const Index: import("svelte").Component<$$ComponentProps, {}, "">;
4
+ type Index = ReturnType<typeof Index>;
24
5
  export default Index;
@@ -18,6 +18,7 @@
18
18
 
19
19
  import {
20
20
  Download,
21
+ DownloadAll,
21
22
  Image as ImageIcon,
22
23
  Clear,
23
24
  Play,
@@ -45,6 +46,7 @@
45
46
  object_fit = "cover",
46
47
  show_share_button = false,
47
48
  show_download_button = false,
49
+ show_download_all_button = false,
48
50
  i18n,
49
51
  selected_index = $bindable(),
50
52
  interactive,
@@ -83,6 +85,7 @@
83
85
  object_fit: "contain" | "cover" | "fill" | "none" | "scale-down";
84
86
  show_share_button: boolean;
85
87
  show_download_button: boolean;
88
+ show_download_all_button: boolean;
86
89
  i18n: I18nFormatter;
87
90
  selected_index: number | null;
88
91
  interactive: boolean;
@@ -112,7 +115,7 @@
112
115
  } = $props();
113
116
 
114
117
  let upload_promise: Promise<any> | null = null;
115
- let mode: "normal" | "minimal" = "normal";
118
+ let mode = $state<"normal" | "minimal">("normal");
116
119
  let display_icon_button_wrapper_top_corner = false;
117
120
  let is_full_screen = false;
118
121
  let image_container: HTMLElement;
@@ -307,6 +310,184 @@
307
310
  URL.revokeObjectURL(url);
308
311
  }
309
312
 
313
+ const crc_table = new Uint32Array(
314
+ Array.from({ length: 256 }, (_, i) => {
315
+ let c = i;
316
+ for (let k = 0; k < 8; k++) {
317
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
318
+ }
319
+ return c >>> 0;
320
+ })
321
+ );
322
+
323
+ function crc32(data: Uint8Array): number {
324
+ let c = 0xffffffff;
325
+ for (const byte of data) {
326
+ c = crc_table[(c ^ byte) & 0xff] ^ (c >>> 8);
327
+ }
328
+ return (c ^ 0xffffffff) >>> 0;
329
+ }
330
+
331
+ function write_uint16(view: DataView, offset: number, value: number): void {
332
+ view.setUint16(offset, value, true);
333
+ }
334
+
335
+ function write_uint32(view: DataView, offset: number, value: number): void {
336
+ view.setUint32(offset, value, true);
337
+ }
338
+
339
+ function zip_header(size: number): Uint8Array {
340
+ return new Uint8Array(size);
341
+ }
342
+
343
+ function get_unique_name(
344
+ file: FileData,
345
+ fallback: string,
346
+ used_names: Map<string, number>
347
+ ): string {
348
+ const raw_name = file.orig_name || file.path || fallback;
349
+ const name = raw_name.split(/[/\\]/).pop() || fallback;
350
+ const count = used_names.get(name) ?? 0;
351
+ used_names.set(name, count + 1);
352
+ if (count === 0) return name;
353
+
354
+ const dot_index = name.lastIndexOf(".");
355
+ const base = dot_index > 0 ? name.slice(0, dot_index) : name;
356
+ const ext = dot_index > 0 ? name.slice(dot_index) : "";
357
+ return `${base}-${count + 1}${ext}`;
358
+ }
359
+
360
+ function create_zip(files: { name: string; data: Uint8Array }[]): Uint8Array {
361
+ const encoder = new TextEncoder();
362
+ const parts: Uint8Array[] = [];
363
+ const central_parts: Uint8Array[] = [];
364
+ const utf8_flag = 0x0800;
365
+ let offset = 0;
366
+
367
+ for (const file of files) {
368
+ const name = encoder.encode(file.name);
369
+ const checksum = crc32(file.data);
370
+ const local_header = zip_header(30 + name.length);
371
+ const local_view = new DataView(local_header.buffer);
372
+ write_uint32(local_view, 0, 0x04034b50);
373
+ write_uint16(local_view, 4, 20);
374
+ write_uint16(local_view, 6, utf8_flag);
375
+ write_uint16(local_view, 8, 0);
376
+ write_uint16(local_view, 10, 0);
377
+ write_uint16(local_view, 12, 33);
378
+ write_uint32(local_view, 14, checksum);
379
+ write_uint32(local_view, 18, file.data.length);
380
+ write_uint32(local_view, 22, file.data.length);
381
+ write_uint16(local_view, 26, name.length);
382
+ write_uint16(local_view, 28, 0);
383
+ local_header.set(name, 30);
384
+ parts.push(local_header, file.data);
385
+
386
+ const central_header = zip_header(46 + name.length);
387
+ const central_view = new DataView(central_header.buffer);
388
+ write_uint32(central_view, 0, 0x02014b50);
389
+ write_uint16(central_view, 4, 20);
390
+ write_uint16(central_view, 6, 20);
391
+ write_uint16(central_view, 8, utf8_flag);
392
+ write_uint16(central_view, 10, 0);
393
+ write_uint16(central_view, 12, 0);
394
+ write_uint16(central_view, 14, 33);
395
+ write_uint32(central_view, 16, checksum);
396
+ write_uint32(central_view, 20, file.data.length);
397
+ write_uint32(central_view, 24, file.data.length);
398
+ write_uint16(central_view, 28, name.length);
399
+ write_uint16(central_view, 30, 0);
400
+ write_uint16(central_view, 32, 0);
401
+ write_uint16(central_view, 34, 0);
402
+ write_uint16(central_view, 36, 0);
403
+ write_uint32(central_view, 38, 0);
404
+ write_uint32(central_view, 42, offset);
405
+ central_header.set(name, 46);
406
+ central_parts.push(central_header);
407
+
408
+ offset += local_header.length + file.data.length;
409
+ }
410
+
411
+ const central_offset = offset;
412
+ const central_size = central_parts.reduce(
413
+ (sum, part) => sum + part.length,
414
+ 0
415
+ );
416
+ const end = zip_header(22);
417
+ const end_view = new DataView(end.buffer);
418
+ write_uint32(end_view, 0, 0x06054b50);
419
+ write_uint16(end_view, 4, 0);
420
+ write_uint16(end_view, 6, 0);
421
+ write_uint16(end_view, 8, files.length);
422
+ write_uint16(end_view, 10, files.length);
423
+ write_uint32(end_view, 12, central_size);
424
+ write_uint32(end_view, 16, central_offset);
425
+ write_uint16(end_view, 20, 0);
426
+
427
+ const zip = new Uint8Array(central_offset + central_size + end.length);
428
+ let cursor = 0;
429
+ for (const part of [...parts, ...central_parts, end]) {
430
+ zip.set(part, cursor);
431
+ cursor += part.length;
432
+ }
433
+ return zip;
434
+ }
435
+
436
+ async function get_zip_file(
437
+ url: string,
438
+ name: string
439
+ ): Promise<{ name: string; data: Uint8Array } | null> {
440
+ let response;
441
+ try {
442
+ response = await _fetch(url);
443
+ } catch (error) {
444
+ if (error instanceof TypeError) {
445
+ window.open(url, "_blank", "noreferrer");
446
+ }
447
+ return null;
448
+ }
449
+ if (!response.ok) return null;
450
+ return {
451
+ name,
452
+ data: new Uint8Array(await response.arrayBuffer())
453
+ };
454
+ }
455
+
456
+ async function download_all(media: GalleryData[]): Promise<void> {
457
+ const used_names = new Map<string, number>();
458
+ const files = await Promise.all(
459
+ media.flatMap((item, index) => {
460
+ const file = "image" in item ? item.image : item.video;
461
+ const fallback =
462
+ "image" in item ? `image-${index + 1}` : `video-${index + 1}`;
463
+ if (!file.url) return [];
464
+ const name = get_unique_name(file, fallback, used_names);
465
+ return [get_zip_file(file.url, name)];
466
+ })
467
+ );
468
+ const valid_files = files.filter(
469
+ (file): file is { name: string; data: Uint8Array } => file !== null
470
+ );
471
+
472
+ if (valid_files.length === 0) return;
473
+
474
+ const zip = create_zip(valid_files);
475
+ const url = URL.createObjectURL(
476
+ new Blob([zip.buffer as ArrayBuffer], { type: "application/zip" })
477
+ );
478
+ const link = document.createElement("a");
479
+ link.href = url;
480
+ link.download = "gallery.zip";
481
+ link.click();
482
+ URL.revokeObjectURL(url);
483
+ }
484
+
485
+ function handle_download_all(): void {
486
+ if (resolved_value) {
487
+ download_all(resolved_value);
488
+ }
489
+ }
490
+
310
491
  let selected_media = $derived.by(() =>
311
492
  selected_index != null && resolved_value != null
312
493
  ? resolved_value[selected_index]
@@ -418,11 +599,11 @@
418
599
  <div class="icon-button">
419
600
  <ShareButton
420
601
  {i18n}
421
- on:share={(detail) => {
422
- onshare(detail);
602
+ onshare={(detail) => {
603
+ onshare({ detail } as CustomEvent);
423
604
  }}
424
- on:error={(detail) => {
425
- onerror(detail);
605
+ onerror={(detail) => {
606
+ onerror({ detail } as CustomEvent);
426
607
  }}
427
608
  value={resolved_value}
428
609
  formatter={format_gallery_for_sharing}
@@ -550,6 +731,13 @@
550
731
  onclear();
551
732
  }}
552
733
  >
734
+ {#if show_download_all_button}
735
+ <IconButton
736
+ Icon={DownloadAll}
737
+ label={i18n("common.download_all")}
738
+ onclick={handle_download_all}
739
+ />
740
+ {/if}
553
741
  {#if upload && stream_handler}
554
742
  <IconButton
555
743
  Icon={UploadIcon}
@@ -598,6 +786,14 @@
598
786
  />
599
787
  {/if}
600
788
  </ModifyUpload>
789
+ {:else if show_download_all_button && !(selected_media && allow_preview)}
790
+ <IconButtonWrapper>
791
+ <IconButton
792
+ Icon={DownloadAll}
793
+ label={i18n("common.download_all")}
794
+ onclick={handle_download_all}
795
+ />
796
+ </IconButtonWrapper>
601
797
  {/if}
602
798
  <div
603
799
  class="grid-container"
@@ -16,6 +16,7 @@ type $$ComponentProps = {
16
16
  object_fit: "contain" | "cover" | "fill" | "none" | "scale-down";
17
17
  show_share_button: boolean;
18
18
  show_download_button: boolean;
19
+ show_download_all_button: boolean;
19
20
  i18n: I18nFormatter;
20
21
  selected_index: number | null;
21
22
  interactive: boolean;
@@ -1,3 +1,3 @@
1
- import { FileData, Client } from "@gradio/client";
1
+ import { FileData } 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[]>;
3
+ export declare function handle_save(img_blob: Blob, upload: (files: FileData[]) => Promise<(FileData | null)[] | null>, filename?: string): Promise<FileData[]>;
@@ -1,5 +1,5 @@
1
1
  import { uploadToHuggingFace } from "@gradio/utils";
2
- import { FileData, Client } from "@gradio/client";
2
+ import { FileData } from "@gradio/client";
3
3
  export async function format_gallery_for_sharing(value) {
4
4
  if (!value)
5
5
  return "";
@@ -25,5 +25,5 @@ export async function handle_save(img_blob, upload, filename = "uploaded_file")
25
25
  is_stream: false
26
26
  })
27
27
  ];
28
- return (await upload(...files));
28
+ return ((await upload(files)) ?? []).filter((file) => file !== null);
29
29
  }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { FileData, SelectData } from "@gradio/client";
2
- import type { CustomButton } from "@gradio/utils";
1
+ import type { FileData } from "@gradio/client";
2
+ import type { CustomButton, SelectData } from "@gradio/utils";
3
3
  export interface GalleryImage {
4
4
  image: FileData;
5
5
  caption: string | null;
@@ -19,7 +19,7 @@ export interface GalleryProps {
19
19
  allow_preview: boolean;
20
20
  selected_index: number | null;
21
21
  object_fit: "contain" | "cover" | "fill" | "none" | "scale-down";
22
- buttons: (string | CustomButton)[];
22
+ buttons: ("share" | "download" | "download_all" | "fullscreen" | CustomButton)[];
23
23
  type: "numpy" | "pil" | "filepath";
24
24
  fit_columns: boolean;
25
25
  sources: ("upload" | "webcam-video" | "webcam" | "clipboard")[];
@@ -34,6 +34,7 @@ export interface GalleryEvents {
34
34
  };
35
35
  preview_open: never;
36
36
  preview_close: never;
37
+ clear: never;
37
38
  clear_status: any;
38
39
  share: any;
39
40
  error: any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/gallery",
3
- "version": "0.17.8",
3
+ "version": "0.19.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -8,15 +8,15 @@
8
8
  "private": false,
9
9
  "dependencies": {
10
10
  "dequal": "^2.0.3",
11
- "@gradio/atoms": "^0.24.0",
12
- "@gradio/file": "^0.14.8",
13
- "@gradio/client": "^2.2.0",
14
- "@gradio/icons": "^0.15.1",
15
- "@gradio/image": "^0.26.3",
16
- "@gradio/statustracker": "^0.14.1",
17
- "@gradio/utils": "^0.12.2",
18
- "@gradio/upload": "^0.17.9",
19
- "@gradio/video": "^0.20.8"
11
+ "@gradio/atoms": "^0.26.0",
12
+ "@gradio/client": "^2.3.1",
13
+ "@gradio/file": "^0.15.1",
14
+ "@gradio/icons": "^0.16.0",
15
+ "@gradio/image": "^0.28.0",
16
+ "@gradio/statustracker": "^0.15.1",
17
+ "@gradio/upload": "^0.18.1",
18
+ "@gradio/utils": "^0.13.0",
19
+ "@gradio/video": "^0.22.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@gradio/preview": "^0.16.2"
@@ -18,6 +18,7 @@
18
18
 
19
19
  import {
20
20
  Download,
21
+ DownloadAll,
21
22
  Image as ImageIcon,
22
23
  Clear,
23
24
  Play,
@@ -45,6 +46,7 @@
45
46
  object_fit = "cover",
46
47
  show_share_button = false,
47
48
  show_download_button = false,
49
+ show_download_all_button = false,
48
50
  i18n,
49
51
  selected_index = $bindable(),
50
52
  interactive,
@@ -83,6 +85,7 @@
83
85
  object_fit: "contain" | "cover" | "fill" | "none" | "scale-down";
84
86
  show_share_button: boolean;
85
87
  show_download_button: boolean;
88
+ show_download_all_button: boolean;
86
89
  i18n: I18nFormatter;
87
90
  selected_index: number | null;
88
91
  interactive: boolean;
@@ -112,7 +115,7 @@
112
115
  } = $props();
113
116
 
114
117
  let upload_promise: Promise<any> | null = null;
115
- let mode: "normal" | "minimal" = "normal";
118
+ let mode = $state<"normal" | "minimal">("normal");
116
119
  let display_icon_button_wrapper_top_corner = false;
117
120
  let is_full_screen = false;
118
121
  let image_container: HTMLElement;
@@ -307,6 +310,184 @@
307
310
  URL.revokeObjectURL(url);
308
311
  }
309
312
 
313
+ const crc_table = new Uint32Array(
314
+ Array.from({ length: 256 }, (_, i) => {
315
+ let c = i;
316
+ for (let k = 0; k < 8; k++) {
317
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
318
+ }
319
+ return c >>> 0;
320
+ })
321
+ );
322
+
323
+ function crc32(data: Uint8Array): number {
324
+ let c = 0xffffffff;
325
+ for (const byte of data) {
326
+ c = crc_table[(c ^ byte) & 0xff] ^ (c >>> 8);
327
+ }
328
+ return (c ^ 0xffffffff) >>> 0;
329
+ }
330
+
331
+ function write_uint16(view: DataView, offset: number, value: number): void {
332
+ view.setUint16(offset, value, true);
333
+ }
334
+
335
+ function write_uint32(view: DataView, offset: number, value: number): void {
336
+ view.setUint32(offset, value, true);
337
+ }
338
+
339
+ function zip_header(size: number): Uint8Array {
340
+ return new Uint8Array(size);
341
+ }
342
+
343
+ function get_unique_name(
344
+ file: FileData,
345
+ fallback: string,
346
+ used_names: Map<string, number>
347
+ ): string {
348
+ const raw_name = file.orig_name || file.path || fallback;
349
+ const name = raw_name.split(/[/\\]/).pop() || fallback;
350
+ const count = used_names.get(name) ?? 0;
351
+ used_names.set(name, count + 1);
352
+ if (count === 0) return name;
353
+
354
+ const dot_index = name.lastIndexOf(".");
355
+ const base = dot_index > 0 ? name.slice(0, dot_index) : name;
356
+ const ext = dot_index > 0 ? name.slice(dot_index) : "";
357
+ return `${base}-${count + 1}${ext}`;
358
+ }
359
+
360
+ function create_zip(files: { name: string; data: Uint8Array }[]): Uint8Array {
361
+ const encoder = new TextEncoder();
362
+ const parts: Uint8Array[] = [];
363
+ const central_parts: Uint8Array[] = [];
364
+ const utf8_flag = 0x0800;
365
+ let offset = 0;
366
+
367
+ for (const file of files) {
368
+ const name = encoder.encode(file.name);
369
+ const checksum = crc32(file.data);
370
+ const local_header = zip_header(30 + name.length);
371
+ const local_view = new DataView(local_header.buffer);
372
+ write_uint32(local_view, 0, 0x04034b50);
373
+ write_uint16(local_view, 4, 20);
374
+ write_uint16(local_view, 6, utf8_flag);
375
+ write_uint16(local_view, 8, 0);
376
+ write_uint16(local_view, 10, 0);
377
+ write_uint16(local_view, 12, 33);
378
+ write_uint32(local_view, 14, checksum);
379
+ write_uint32(local_view, 18, file.data.length);
380
+ write_uint32(local_view, 22, file.data.length);
381
+ write_uint16(local_view, 26, name.length);
382
+ write_uint16(local_view, 28, 0);
383
+ local_header.set(name, 30);
384
+ parts.push(local_header, file.data);
385
+
386
+ const central_header = zip_header(46 + name.length);
387
+ const central_view = new DataView(central_header.buffer);
388
+ write_uint32(central_view, 0, 0x02014b50);
389
+ write_uint16(central_view, 4, 20);
390
+ write_uint16(central_view, 6, 20);
391
+ write_uint16(central_view, 8, utf8_flag);
392
+ write_uint16(central_view, 10, 0);
393
+ write_uint16(central_view, 12, 0);
394
+ write_uint16(central_view, 14, 33);
395
+ write_uint32(central_view, 16, checksum);
396
+ write_uint32(central_view, 20, file.data.length);
397
+ write_uint32(central_view, 24, file.data.length);
398
+ write_uint16(central_view, 28, name.length);
399
+ write_uint16(central_view, 30, 0);
400
+ write_uint16(central_view, 32, 0);
401
+ write_uint16(central_view, 34, 0);
402
+ write_uint16(central_view, 36, 0);
403
+ write_uint32(central_view, 38, 0);
404
+ write_uint32(central_view, 42, offset);
405
+ central_header.set(name, 46);
406
+ central_parts.push(central_header);
407
+
408
+ offset += local_header.length + file.data.length;
409
+ }
410
+
411
+ const central_offset = offset;
412
+ const central_size = central_parts.reduce(
413
+ (sum, part) => sum + part.length,
414
+ 0
415
+ );
416
+ const end = zip_header(22);
417
+ const end_view = new DataView(end.buffer);
418
+ write_uint32(end_view, 0, 0x06054b50);
419
+ write_uint16(end_view, 4, 0);
420
+ write_uint16(end_view, 6, 0);
421
+ write_uint16(end_view, 8, files.length);
422
+ write_uint16(end_view, 10, files.length);
423
+ write_uint32(end_view, 12, central_size);
424
+ write_uint32(end_view, 16, central_offset);
425
+ write_uint16(end_view, 20, 0);
426
+
427
+ const zip = new Uint8Array(central_offset + central_size + end.length);
428
+ let cursor = 0;
429
+ for (const part of [...parts, ...central_parts, end]) {
430
+ zip.set(part, cursor);
431
+ cursor += part.length;
432
+ }
433
+ return zip;
434
+ }
435
+
436
+ async function get_zip_file(
437
+ url: string,
438
+ name: string
439
+ ): Promise<{ name: string; data: Uint8Array } | null> {
440
+ let response;
441
+ try {
442
+ response = await _fetch(url);
443
+ } catch (error) {
444
+ if (error instanceof TypeError) {
445
+ window.open(url, "_blank", "noreferrer");
446
+ }
447
+ return null;
448
+ }
449
+ if (!response.ok) return null;
450
+ return {
451
+ name,
452
+ data: new Uint8Array(await response.arrayBuffer())
453
+ };
454
+ }
455
+
456
+ async function download_all(media: GalleryData[]): Promise<void> {
457
+ const used_names = new Map<string, number>();
458
+ const files = await Promise.all(
459
+ media.flatMap((item, index) => {
460
+ const file = "image" in item ? item.image : item.video;
461
+ const fallback =
462
+ "image" in item ? `image-${index + 1}` : `video-${index + 1}`;
463
+ if (!file.url) return [];
464
+ const name = get_unique_name(file, fallback, used_names);
465
+ return [get_zip_file(file.url, name)];
466
+ })
467
+ );
468
+ const valid_files = files.filter(
469
+ (file): file is { name: string; data: Uint8Array } => file !== null
470
+ );
471
+
472
+ if (valid_files.length === 0) return;
473
+
474
+ const zip = create_zip(valid_files);
475
+ const url = URL.createObjectURL(
476
+ new Blob([zip.buffer as ArrayBuffer], { type: "application/zip" })
477
+ );
478
+ const link = document.createElement("a");
479
+ link.href = url;
480
+ link.download = "gallery.zip";
481
+ link.click();
482
+ URL.revokeObjectURL(url);
483
+ }
484
+
485
+ function handle_download_all(): void {
486
+ if (resolved_value) {
487
+ download_all(resolved_value);
488
+ }
489
+ }
490
+
310
491
  let selected_media = $derived.by(() =>
311
492
  selected_index != null && resolved_value != null
312
493
  ? resolved_value[selected_index]
@@ -418,11 +599,11 @@
418
599
  <div class="icon-button">
419
600
  <ShareButton
420
601
  {i18n}
421
- on:share={(detail) => {
422
- onshare(detail);
602
+ onshare={(detail) => {
603
+ onshare({ detail } as CustomEvent);
423
604
  }}
424
- on:error={(detail) => {
425
- onerror(detail);
605
+ onerror={(detail) => {
606
+ onerror({ detail } as CustomEvent);
426
607
  }}
427
608
  value={resolved_value}
428
609
  formatter={format_gallery_for_sharing}
@@ -550,6 +731,13 @@
550
731
  onclear();
551
732
  }}
552
733
  >
734
+ {#if show_download_all_button}
735
+ <IconButton
736
+ Icon={DownloadAll}
737
+ label={i18n("common.download_all")}
738
+ onclick={handle_download_all}
739
+ />
740
+ {/if}
553
741
  {#if upload && stream_handler}
554
742
  <IconButton
555
743
  Icon={UploadIcon}
@@ -598,6 +786,14 @@
598
786
  />
599
787
  {/if}
600
788
  </ModifyUpload>
789
+ {:else if show_download_all_button && !(selected_media && allow_preview)}
790
+ <IconButtonWrapper>
791
+ <IconButton
792
+ Icon={DownloadAll}
793
+ label={i18n("common.download_all")}
794
+ onclick={handle_download_all}
795
+ />
796
+ </IconButtonWrapper>
601
797
  {/if}
602
798
  <div
603
799
  class="grid-container"
package/shared/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { uploadToHuggingFace } from "@gradio/utils";
2
- import { FileData, Client } from "@gradio/client";
2
+ import { FileData } from "@gradio/client";
3
3
 
4
4
  export async function format_gallery_for_sharing(
5
5
  value: [FileData, string | null][] | null
@@ -19,8 +19,8 @@ export async function format_gallery_for_sharing(
19
19
 
20
20
  export async function handle_save(
21
21
  img_blob: Blob,
22
- upload: Client["upload"],
23
- filename: string = "uploaded_file"
22
+ upload: (files: FileData[]) => Promise<(FileData | null)[] | null>,
23
+ filename = "uploaded_file"
24
24
  ): Promise<FileData[]> {
25
25
  const ext = img_blob.type.split("/")[1] || "png";
26
26
  const f_ = new File([img_blob], `${filename}.${ext}`);
@@ -34,5 +34,7 @@ export async function handle_save(
34
34
  is_stream: false
35
35
  })
36
36
  ];
37
- return (await upload(...files)) as FileData[];
37
+ return ((await upload(files)) ?? []).filter(
38
+ (file): file is FileData => file !== null
39
+ );
38
40
  }
package/types.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { FileData, SelectData } from "@gradio/client";
2
- import type { CustomButton } from "@gradio/utils";
1
+ import type { FileData } from "@gradio/client";
2
+ import type { CustomButton, SelectData } from "@gradio/utils";
3
3
 
4
4
  export interface GalleryImage {
5
5
  image: FileData;
@@ -23,7 +23,13 @@ export interface GalleryProps {
23
23
  allow_preview: boolean;
24
24
  selected_index: number | null;
25
25
  object_fit: "contain" | "cover" | "fill" | "none" | "scale-down";
26
- buttons: (string | CustomButton)[];
26
+ buttons: (
27
+ | "share"
28
+ | "download"
29
+ | "download_all"
30
+ | "fullscreen"
31
+ | CustomButton
32
+ )[];
27
33
  type: "numpy" | "pil" | "filepath";
28
34
  fit_columns: boolean;
29
35
  sources: ("upload" | "webcam-video" | "webcam" | "clipboard")[];
@@ -36,6 +42,7 @@ export interface GalleryEvents {
36
42
  delete: { file: FileData; index: number };
37
43
  preview_open: never;
38
44
  preview_close: never;
45
+ clear: never;
39
46
  clear_status: any;
40
47
  share: any;
41
48
  error: any;