@gradio/gallery 0.18.0 → 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 +18 -0
- package/Gallery.test.ts +111 -1
- package/Index.svelte +9 -12
- package/dist/Index.svelte +9 -12
- package/dist/Index.svelte.d.ts +2 -21
- package/dist/shared/Gallery.svelte +200 -4
- package/dist/shared/Gallery.svelte.d.ts +1 -0
- package/dist/types.d.ts +1 -1
- package/package.json +9 -9
- package/shared/Gallery.svelte +200 -4
- package/types.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
3
21
|
## 0.18.0
|
|
4
22
|
|
|
5
23
|
### Features
|
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
|
@@ -205,10 +205,10 @@
|
|
|
205
205
|
<Webcam
|
|
206
206
|
root={gradio.shared.root}
|
|
207
207
|
value={null}
|
|
208
|
-
|
|
209
|
-
if (!(
|
|
208
|
+
oncapture={async (detail) => {
|
|
209
|
+
if (!(detail instanceof Blob)) return;
|
|
210
210
|
const f = await handle_save(
|
|
211
|
-
|
|
211
|
+
detail,
|
|
212
212
|
(f) => gradio.shared.client.upload(f, gradio.shared.root),
|
|
213
213
|
"webcam_upload"
|
|
214
214
|
);
|
|
@@ -217,9 +217,6 @@
|
|
|
217
217
|
active_source = null;
|
|
218
218
|
gradio.dispatch("change", gradio.props.value);
|
|
219
219
|
}}
|
|
220
|
-
on:error
|
|
221
|
-
on:drag
|
|
222
|
-
on:close_stream
|
|
223
220
|
mirror_webcam={true}
|
|
224
221
|
streaming={false}
|
|
225
222
|
mode="image"
|
|
@@ -231,18 +228,15 @@
|
|
|
231
228
|
<Webcam
|
|
232
229
|
root={gradio.shared.root}
|
|
233
230
|
value={null}
|
|
234
|
-
|
|
235
|
-
if (!
|
|
236
|
-
const f = { ...(
|
|
231
|
+
oncapture={async (detail) => {
|
|
232
|
+
if (!detail) return;
|
|
233
|
+
const f = { ...(detail as FileData) };
|
|
237
234
|
f.mime_type = "video/webm";
|
|
238
235
|
const processed_files = await process_upload_files([f]);
|
|
239
236
|
gradio.props.value?.push(...processed_files);
|
|
240
237
|
active_source = null;
|
|
241
238
|
gradio.dispatch("change", gradio.props.value);
|
|
242
239
|
}}
|
|
243
|
-
on:error
|
|
244
|
-
on:drag
|
|
245
|
-
on:close_stream
|
|
246
240
|
mirror_webcam={true}
|
|
247
241
|
streaming={false}
|
|
248
242
|
mode="video"
|
|
@@ -302,6 +296,9 @@
|
|
|
302
296
|
show_download_button={gradio.props.buttons.some(
|
|
303
297
|
(btn) => typeof btn === "string" && btn === "download"
|
|
304
298
|
)}
|
|
299
|
+
show_download_all_button={gradio.props.buttons.some(
|
|
300
|
+
(btn) => typeof btn === "string" && btn === "download_all"
|
|
301
|
+
)}
|
|
305
302
|
fit_columns={gradio.props.fit_columns}
|
|
306
303
|
i18n={gradio.i18n}
|
|
307
304
|
_fetch={(...args) => gradio.shared.client.fetch(...args)}
|
package/dist/Index.svelte
CHANGED
|
@@ -205,10 +205,10 @@
|
|
|
205
205
|
<Webcam
|
|
206
206
|
root={gradio.shared.root}
|
|
207
207
|
value={null}
|
|
208
|
-
|
|
209
|
-
if (!(
|
|
208
|
+
oncapture={async (detail) => {
|
|
209
|
+
if (!(detail instanceof Blob)) return;
|
|
210
210
|
const f = await handle_save(
|
|
211
|
-
|
|
211
|
+
detail,
|
|
212
212
|
(f) => gradio.shared.client.upload(f, gradio.shared.root),
|
|
213
213
|
"webcam_upload"
|
|
214
214
|
);
|
|
@@ -217,9 +217,6 @@
|
|
|
217
217
|
active_source = null;
|
|
218
218
|
gradio.dispatch("change", gradio.props.value);
|
|
219
219
|
}}
|
|
220
|
-
on:error
|
|
221
|
-
on:drag
|
|
222
|
-
on:close_stream
|
|
223
220
|
mirror_webcam={true}
|
|
224
221
|
streaming={false}
|
|
225
222
|
mode="image"
|
|
@@ -231,18 +228,15 @@
|
|
|
231
228
|
<Webcam
|
|
232
229
|
root={gradio.shared.root}
|
|
233
230
|
value={null}
|
|
234
|
-
|
|
235
|
-
if (!
|
|
236
|
-
const f = { ...(
|
|
231
|
+
oncapture={async (detail) => {
|
|
232
|
+
if (!detail) return;
|
|
233
|
+
const f = { ...(detail as FileData) };
|
|
237
234
|
f.mime_type = "video/webm";
|
|
238
235
|
const processed_files = await process_upload_files([f]);
|
|
239
236
|
gradio.props.value?.push(...processed_files);
|
|
240
237
|
active_source = null;
|
|
241
238
|
gradio.dispatch("change", gradio.props.value);
|
|
242
239
|
}}
|
|
243
|
-
on:error
|
|
244
|
-
on:drag
|
|
245
|
-
on:close_stream
|
|
246
240
|
mirror_webcam={true}
|
|
247
241
|
streaming={false}
|
|
248
242
|
mode="video"
|
|
@@ -302,6 +296,9 @@
|
|
|
302
296
|
show_download_button={gradio.props.buttons.some(
|
|
303
297
|
(btn) => typeof btn === "string" && btn === "download"
|
|
304
298
|
)}
|
|
299
|
+
show_download_all_button={gradio.props.buttons.some(
|
|
300
|
+
(btn) => typeof btn === "string" && btn === "download_all"
|
|
301
|
+
)}
|
|
305
302
|
fit_columns={gradio.props.fit_columns}
|
|
306
303
|
i18n={gradio.i18n}
|
|
307
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;
|
|
@@ -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/types.d.ts
CHANGED
|
@@ -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")[];
|
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/
|
|
15
|
-
"@gradio/image": "^0.
|
|
16
|
-
"@gradio/statustracker": "^0.15.
|
|
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",
|
|
17
18
|
"@gradio/utils": "^0.13.0",
|
|
18
|
-
"@gradio/
|
|
19
|
-
"@gradio/video": "^0.21.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;
|
|
@@ -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/types.ts
CHANGED
|
@@ -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")[];
|