@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 +48 -0
- package/Gallery.stories.svelte +0 -180
- package/Gallery.test.ts +111 -1
- package/Index.svelte +13 -14
- package/dist/Index.svelte +13 -14
- package/dist/Index.svelte.d.ts +2 -21
- package/dist/shared/Gallery.svelte +201 -5
- package/dist/shared/Gallery.svelte.d.ts +1 -0
- package/dist/shared/utils.d.ts +2 -2
- package/dist/shared/utils.js +2 -2
- package/dist/types.d.ts +4 -3
- package/package.json +10 -10
- package/shared/Gallery.svelte +201 -5
- package/shared/utils.ts +6 -4
- package/types.ts +10 -3
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
|
package/Gallery.stories.svelte
CHANGED
|
@@ -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 {
|
|
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
|
|
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={(
|
|
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
|
-
|
|
208
|
+
oncapture={async (detail) => {
|
|
209
|
+
if (!(detail instanceof Blob)) return;
|
|
209
210
|
const f = await handle_save(
|
|
210
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
|
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={(
|
|
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
|
-
|
|
208
|
+
oncapture={async (detail) => {
|
|
209
|
+
if (!(detail instanceof Blob)) return;
|
|
209
210
|
const f = await handle_save(
|
|
210
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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.d.ts
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
1
|
export { default as BaseGallery } from "./shared/Gallery.svelte";
|
|
2
2
|
export { default as BaseExample } from "./Example.svelte";
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
422
|
-
onshare(detail);
|
|
602
|
+
onshare={(detail) => {
|
|
603
|
+
onshare({ detail } as CustomEvent);
|
|
423
604
|
}}
|
|
424
|
-
|
|
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;
|
package/dist/shared/utils.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { FileData
|
|
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:
|
|
3
|
+
export declare function handle_save(img_blob: Blob, upload: (files: FileData[]) => Promise<(FileData | null)[] | null>, filename?: string): Promise<FileData[]>;
|
package/dist/shared/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { uploadToHuggingFace } from "@gradio/utils";
|
|
2
|
-
import { FileData
|
|
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(
|
|
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
|
|
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: (
|
|
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.
|
|
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.
|
|
12
|
-
"@gradio/
|
|
13
|
-
"@gradio/
|
|
14
|
-
"@gradio/icons": "^0.
|
|
15
|
-
"@gradio/image": "^0.
|
|
16
|
-
"@gradio/statustracker": "^0.
|
|
17
|
-
"@gradio/
|
|
18
|
-
"@gradio/
|
|
19
|
-
"@gradio/video": "^0.
|
|
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"
|
package/shared/Gallery.svelte
CHANGED
|
@@ -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
|
|
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
|
-
|
|
422
|
-
onshare(detail);
|
|
602
|
+
onshare={(detail) => {
|
|
603
|
+
onshare({ detail } as CustomEvent);
|
|
423
604
|
}}
|
|
424
|
-
|
|
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
|
|
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:
|
|
23
|
-
filename
|
|
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(
|
|
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
|
|
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: (
|
|
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;
|