@gradio/gallery 0.17.7 → 0.18.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,41 @@
1
1
  # @gradio/gallery
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Features
6
+
7
+ - [#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!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/atoms@0.25.0
12
+ - @gradio/statustracker@0.15.0
13
+ - @gradio/utils@0.13.0
14
+ - @gradio/client@2.3.0
15
+ - @gradio/upload@0.18.0
16
+ - @gradio/image@0.27.0
17
+ - @gradio/file@0.15.0
18
+ - @gradio/video@0.21.0
19
+
20
+ ## 0.17.8
21
+
22
+ ### Dependency updates
23
+
24
+ - @gradio/client@2.2.2
25
+
26
+ ## 0.17.8
27
+
28
+ ### Dependency updates
29
+
30
+ - @gradio/client@2.2.1
31
+ - @gradio/upload@0.17.10
32
+
33
+ ## 0.17.8
34
+
35
+ ### Fixes
36
+
37
+ - [#13335](https://github.com/gradio-app/gradio/pull/13335) [`953efe0`](https://github.com/gradio-app/gradio/commit/953efe06fa6d0742a9dd899b846d45fbc6ea3ccf) - Prevent Gallery Select Event From Triggering When Value Updated Programmatically. Thanks @freddyaboulton!
38
+
3
39
  ## 0.17.7
4
40
 
5
41
  ### Dependency updates
@@ -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
@@ -627,7 +627,7 @@ describe("Events: select", () => {
627
627
  expect(select).toHaveBeenCalledWith(expect.objectContaining({ index: 1 }));
628
628
  });
629
629
 
630
- test("select: fired when selected_index changes via set_data", async () => {
630
+ test("select: not fired when selected_index changes via set_data", async () => {
631
631
  const { listen, set_data } = await render(Gallery, {
632
632
  ...default_props,
633
633
  preview: true,
@@ -639,8 +639,25 @@ describe("Events: select", () => {
639
639
 
640
640
  await set_data({ selected_index: 2 });
641
641
 
642
- expect(select).toHaveBeenCalled();
643
- expect(select).toHaveBeenCalledWith(expect.objectContaining({ index: 2 }));
642
+ expect(select).not.toHaveBeenCalled();
643
+ });
644
+
645
+ test("select: not fired when value and selected_index update together via set_data", async () => {
646
+ const { listen, set_data } = await render(Gallery, {
647
+ ...default_props,
648
+ preview: true,
649
+ selected_index: 0,
650
+ value: [img("a"), img("b")]
651
+ });
652
+
653
+ const select = listen("select");
654
+
655
+ await set_data({
656
+ value: [img("a"), img("b"), img("c")],
657
+ selected_index: 2
658
+ });
659
+
660
+ expect(select).not.toHaveBeenCalled();
644
661
  });
645
662
  });
646
663
 
@@ -976,7 +993,7 @@ describe("Regression: #13170 — selected_index points to newly appended image",
976
993
  expect(preview_img.src).toBe("https://example.com/img3.png");
977
994
  });
978
995
 
979
- test("selected_index pointing to newly appended item dispatches select with correct data", async () => {
996
+ test("selected_index pointing to newly appended item does not dispatch select", async () => {
980
997
  const initial_value = [img("a"), img("b")];
981
998
 
982
999
  const { set_data, listen } = await render(Gallery, {
@@ -993,9 +1010,7 @@ describe("Regression: #13170 — selected_index points to newly appended image",
993
1010
  selected_index: 2
994
1011
  });
995
1012
 
996
- expect(select).toHaveBeenCalled();
997
- const last_call = select.mock.calls[select.mock.calls.length - 1][0];
998
- expect(last_call.index).toBe(2);
1013
+ expect(select).not.toHaveBeenCalled();
999
1014
  });
1000
1015
 
1001
1016
  test("rapidly appending images with selected_index tracking last always shows the latest", async () => {
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);
@@ -206,6 +206,7 @@
206
206
  root={gradio.shared.root}
207
207
  value={null}
208
208
  on:capture={async (e) => {
209
+ if (!(e.detail instanceof Blob)) return;
209
210
  const f = await handle_save(
210
211
  e.detail,
211
212
  (f) => gradio.shared.client.upload(f, gradio.shared.root),
@@ -231,7 +232,8 @@
231
232
  root={gradio.shared.root}
232
233
  value={null}
233
234
  on:capture={async (e) => {
234
- const f = { ...e.detail };
235
+ if (!e.detail) return;
236
+ const f = { ...(e.detail as FileData) };
235
237
  f.mime_type = "video/webm";
236
238
  const processed_files = await process_upload_files([f]);
237
239
  gradio.props.value?.push(...processed_files);
@@ -254,7 +256,7 @@
254
256
  {sources}
255
257
  bind:active_source
256
258
  handle_clear={() => gradio.dispatch("clear")}
257
- handle_select={handle_select_source}
259
+ handle_select={(source) => handle_select_source(source as any)}
258
260
  />
259
261
  {/if}
260
262
  {:else}
@@ -282,7 +284,7 @@
282
284
  gradio.dispatch("change", gradio.props.value);
283
285
  }}
284
286
  {sources}
285
- {onsource_change}
287
+ onsource_change={(source) => onsource_change(source as any)}
286
288
  label={gradio.shared.label}
287
289
  show_label={gradio.shared.show_label}
288
290
  columns={gradio.props.columns}
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);
@@ -206,6 +206,7 @@
206
206
  root={gradio.shared.root}
207
207
  value={null}
208
208
  on:capture={async (e) => {
209
+ if (!(e.detail instanceof Blob)) return;
209
210
  const f = await handle_save(
210
211
  e.detail,
211
212
  (f) => gradio.shared.client.upload(f, gradio.shared.root),
@@ -231,7 +232,8 @@
231
232
  root={gradio.shared.root}
232
233
  value={null}
233
234
  on:capture={async (e) => {
234
- const f = { ...e.detail };
235
+ if (!e.detail) return;
236
+ const f = { ...(e.detail as FileData) };
235
237
  f.mime_type = "video/webm";
236
238
  const processed_files = await process_upload_files([f]);
237
239
  gradio.props.value?.push(...processed_files);
@@ -254,7 +256,7 @@
254
256
  {sources}
255
257
  bind:active_source
256
258
  handle_clear={() => gradio.dispatch("clear")}
257
- handle_select={handle_select_source}
259
+ handle_select={(source) => handle_select_source(source as any)}
258
260
  />
259
261
  {/if}
260
262
  {:else}
@@ -282,7 +284,7 @@
282
284
  gradio.dispatch("change", gradio.props.value);
283
285
  }}
284
286
  {sources}
285
- {onsource_change}
287
+ onsource_change={(source) => onsource_change(source as any)}
286
288
  label={gradio.shared.label}
287
289
  show_label={gradio.shared.show_label}
288
290
  columns={gradio.props.columns}
@@ -112,7 +112,7 @@
112
112
  } = $props();
113
113
 
114
114
  let upload_promise: Promise<any> | null = null;
115
- let mode: "normal" | "minimal" = "normal";
115
+ let mode = $state<"normal" | "minimal">("normal");
116
116
  let display_icon_button_wrapper_top_corner = false;
117
117
  let is_full_screen = false;
118
118
  let image_container: HTMLElement;
@@ -160,7 +160,6 @@
160
160
  if (selected_index == null && preview && value?.length) {
161
161
  selected_index = 0;
162
162
  }
163
- let old_selected_index: number | null = $state(selected_index);
164
163
 
165
164
  $effect(() => {
166
165
  if (!dequal(prev_value, value)) {
@@ -195,6 +194,15 @@
195
194
  () => ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0)
196
195
  );
197
196
 
197
+ function dispatch_select(index: number): void {
198
+ if (resolved_value !== null) {
199
+ onselect({
200
+ index,
201
+ value: resolved_value?.[index]
202
+ });
203
+ }
204
+ }
205
+
198
206
  function handle_preview_click(event: MouseEvent): void {
199
207
  const element = event.target as HTMLElement;
200
208
  const x = event.offsetX;
@@ -206,6 +214,7 @@
206
214
  } else {
207
215
  selected_index = next;
208
216
  }
217
+ dispatch_select(selected_index);
209
218
  }
210
219
 
211
220
  function on_keydown(e: KeyboardEvent): void {
@@ -218,32 +227,18 @@
218
227
  case "ArrowLeft":
219
228
  e.preventDefault();
220
229
  selected_index = previous;
230
+ dispatch_select(selected_index);
221
231
  break;
222
232
  case "ArrowRight":
223
233
  e.preventDefault();
224
234
  selected_index = next;
235
+ dispatch_select(selected_index);
225
236
  break;
226
237
  default:
227
238
  break;
228
239
  }
229
240
  }
230
241
 
231
- $effect(() => {
232
- // Only trigger the effect if the selected_index has actually changed
233
- if (selected_index !== old_selected_index) {
234
- old_selected_index = selected_index;
235
-
236
- // Ensure we have a valid selection and data to work with
237
- if (selected_index !== null && resolved_value !== null) {
238
- // Notify the parent component or listener about the updated selection
239
- onselect({
240
- index: selected_index,
241
- value: resolved_value?.[selected_index]
242
- });
243
- }
244
- }
245
- });
246
-
247
242
  $effect(() => {
248
243
  if (allow_preview && container_element) {
249
244
  scroll_to_img(selected_index);
@@ -495,7 +490,10 @@
495
490
  {#each resolved_value as media, i}
496
491
  <button
497
492
  bind:this={el[i]}
498
- on:click={() => (selected_index = i)}
493
+ on:click={() => {
494
+ selected_index = i;
495
+ dispatch_select(i);
496
+ }}
499
497
  class="thumbnail-item thumbnail-small"
500
498
  class:selected={selected_index === i && mode !== "minimal"}
501
499
  aria-label={"Thumbnail " +
@@ -616,6 +614,7 @@
616
614
  onpreview_open();
617
615
  }
618
616
  selected_index = i;
617
+ dispatch_select(i);
619
618
  }}
620
619
  aria-label={"Thumbnail " +
621
620
  (i + 1) +
@@ -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;
@@ -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.7",
3
+ "version": "0.18.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/client": "^2.2.0",
11
+ "@gradio/atoms": "^0.25.0",
12
+ "@gradio/file": "^0.15.0",
13
13
  "@gradio/icons": "^0.15.1",
14
- "@gradio/file": "^0.14.8",
15
- "@gradio/utils": "^0.12.2",
16
- "@gradio/image": "^0.26.3",
17
- "@gradio/upload": "^0.17.9",
18
- "@gradio/video": "^0.20.8",
19
- "@gradio/statustracker": "^0.14.1"
14
+ "@gradio/client": "^2.3.0",
15
+ "@gradio/image": "^0.27.0",
16
+ "@gradio/statustracker": "^0.15.0",
17
+ "@gradio/utils": "^0.13.0",
18
+ "@gradio/upload": "^0.18.0",
19
+ "@gradio/video": "^0.21.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@gradio/preview": "^0.16.2"
@@ -112,7 +112,7 @@
112
112
  } = $props();
113
113
 
114
114
  let upload_promise: Promise<any> | null = null;
115
- let mode: "normal" | "minimal" = "normal";
115
+ let mode = $state<"normal" | "minimal">("normal");
116
116
  let display_icon_button_wrapper_top_corner = false;
117
117
  let is_full_screen = false;
118
118
  let image_container: HTMLElement;
@@ -160,7 +160,6 @@
160
160
  if (selected_index == null && preview && value?.length) {
161
161
  selected_index = 0;
162
162
  }
163
- let old_selected_index: number | null = $state(selected_index);
164
163
 
165
164
  $effect(() => {
166
165
  if (!dequal(prev_value, value)) {
@@ -195,6 +194,15 @@
195
194
  () => ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0)
196
195
  );
197
196
 
197
+ function dispatch_select(index: number): void {
198
+ if (resolved_value !== null) {
199
+ onselect({
200
+ index,
201
+ value: resolved_value?.[index]
202
+ });
203
+ }
204
+ }
205
+
198
206
  function handle_preview_click(event: MouseEvent): void {
199
207
  const element = event.target as HTMLElement;
200
208
  const x = event.offsetX;
@@ -206,6 +214,7 @@
206
214
  } else {
207
215
  selected_index = next;
208
216
  }
217
+ dispatch_select(selected_index);
209
218
  }
210
219
 
211
220
  function on_keydown(e: KeyboardEvent): void {
@@ -218,32 +227,18 @@
218
227
  case "ArrowLeft":
219
228
  e.preventDefault();
220
229
  selected_index = previous;
230
+ dispatch_select(selected_index);
221
231
  break;
222
232
  case "ArrowRight":
223
233
  e.preventDefault();
224
234
  selected_index = next;
235
+ dispatch_select(selected_index);
225
236
  break;
226
237
  default:
227
238
  break;
228
239
  }
229
240
  }
230
241
 
231
- $effect(() => {
232
- // Only trigger the effect if the selected_index has actually changed
233
- if (selected_index !== old_selected_index) {
234
- old_selected_index = selected_index;
235
-
236
- // Ensure we have a valid selection and data to work with
237
- if (selected_index !== null && resolved_value !== null) {
238
- // Notify the parent component or listener about the updated selection
239
- onselect({
240
- index: selected_index,
241
- value: resolved_value?.[selected_index]
242
- });
243
- }
244
- }
245
- });
246
-
247
242
  $effect(() => {
248
243
  if (allow_preview && container_element) {
249
244
  scroll_to_img(selected_index);
@@ -495,7 +490,10 @@
495
490
  {#each resolved_value as media, i}
496
491
  <button
497
492
  bind:this={el[i]}
498
- on:click={() => (selected_index = i)}
493
+ on:click={() => {
494
+ selected_index = i;
495
+ dispatch_select(i);
496
+ }}
499
497
  class="thumbnail-item thumbnail-small"
500
498
  class:selected={selected_index === i && mode !== "minimal"}
501
499
  aria-label={"Thumbnail " +
@@ -616,6 +614,7 @@
616
614
  onpreview_open();
617
615
  }
618
616
  selected_index = i;
617
+ dispatch_select(i);
619
618
  }}
620
619
  aria-label={"Thumbnail " +
621
620
  (i + 1) +
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;
@@ -36,6 +36,7 @@ export interface GalleryEvents {
36
36
  delete: { file: FileData; index: number };
37
37
  preview_open: never;
38
38
  preview_close: never;
39
+ clear: never;
39
40
  clear_status: any;
40
41
  share: any;
41
42
  error: any;