@gradio/gallery 0.12.2 → 0.13.0-beta.2

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,44 @@
1
1
  # @gradio/gallery
2
2
 
3
+ ## 0.13.0-beta.2
4
+
5
+ ### Features
6
+
7
+ - [#9339](https://github.com/gradio-app/gradio/pull/9339) [`4c8c6f2`](https://github.com/gradio-app/gradio/commit/4c8c6f2fe603081941c5fdc43f48a0632b9f31ad) - Ssr part 2. Thanks @pngwn!
8
+ - [#9250](https://github.com/gradio-app/gradio/pull/9250) [`350b0a5`](https://github.com/gradio-app/gradio/commit/350b0a5cafb9176f914f62e7c90de51d4352cc77) - Improve Icon Button consistency. Thanks @hannahblair!
9
+
10
+ ### Dependency updates
11
+
12
+ - @gradio/atoms@0.9.0-beta.2
13
+ - @gradio/upload@0.13.0-beta.2
14
+ - @gradio/client@1.6.0-beta.2
15
+ - @gradio/icons@0.8.0-beta.2
16
+ - @gradio/statustracker@0.8.0-beta.2
17
+ - @gradio/utils@0.7.0-beta.2
18
+ - @gradio/file@0.10.0-beta.2
19
+ - @gradio/image@0.16.0-beta.2
20
+ - @gradio/video@0.11.0-beta.2
21
+
22
+ ## 0.13.0-beta.1
23
+
24
+ ### Dependency updates
25
+
26
+ - @gradio/video@0.11.0-beta.1
27
+ - @gradio/atoms@0.8.1-beta.1
28
+ - @gradio/icons@0.8.0-beta.1
29
+ - @gradio/statustracker@0.8.0-beta.1
30
+ - @gradio/utils@0.7.0-beta.1
31
+ - @gradio/client@1.6.0-beta.1
32
+ - @gradio/image@0.16.0-beta.1
33
+ - @gradio/upload@0.12.4-beta.1
34
+ - @gradio/file@0.9.4-beta.1
35
+
36
+ ## 0.13.0-beta.0
37
+
38
+ ### Features
39
+
40
+ - [#9052](https://github.com/gradio-app/gradio/pull/9052) [`f3652eb`](https://github.com/gradio-app/gradio/commit/f3652ebe08211e12739df73c15fd97e5ff81276a) - Video gallery. Thanks @dawoodkhan82!
41
+
3
42
  ## 0.12.2
4
43
 
5
44
  ### Fixes
@@ -86,6 +86,14 @@
86
86
  },
87
87
  caption: "A fast cheetah"
88
88
  },
89
+ {
90
+ video: {
91
+ path: "https://gradio-builds.s3.amazonaws.com/demo-files/world.mp4",
92
+ url: "https://gradio-builds.s3.amazonaws.com/demo-files/world.mp4",
93
+ orig_name: "world.mp4"
94
+ },
95
+ caption: "The world"
96
+ },
89
97
  {
90
98
  image: {
91
99
  path: "https://gradio-builds.s3.amazonaws.com/demo-files/cheetah-002.jpg",
@@ -139,7 +147,7 @@
139
147
  play={async ({ canvasElement }) => {
140
148
  const canvas = within(canvasElement);
141
149
 
142
- const image = canvas.getByLabelText("Thumbnail 1 of 7");
150
+ const image = canvas.getByLabelText("Thumbnail 1 of 8");
143
151
  await userEvent.click(image);
144
152
  const expand_btn = canvas.getByRole("button", {
145
153
  name: "View in full screen"
package/Gallery.test.ts CHANGED
@@ -33,8 +33,8 @@ describe("Gallery", () => {
33
33
  value: [
34
34
  {
35
35
  image: {
36
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
37
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
36
+ path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/cheetah.jpg",
37
+ url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/cheetah.jpg"
38
38
  },
39
39
  caption: null
40
40
  }
@@ -43,7 +43,30 @@ describe("Gallery", () => {
43
43
  let item = getByTestId("detailed-image") as HTMLImageElement;
44
44
  assert.equal(
45
45
  item.src,
46
- "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
46
+ "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/cheetah.jpg"
47
+ );
48
+ });
49
+
50
+ test("renders the video provided", async () => {
51
+ const { getByTestId } = await render(Gallery, {
52
+ show_label: true,
53
+ label: "Gallery",
54
+ loading_status: loading_status,
55
+ preview: true,
56
+ value: [
57
+ {
58
+ video: {
59
+ path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/world.mp4",
60
+ url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/world.mp4"
61
+ },
62
+ caption: null
63
+ }
64
+ ]
65
+ });
66
+ let item = getByTestId("detailed-video") as HTMLVideoElement;
67
+ assert.equal(
68
+ item.src,
69
+ "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/world.mp4"
47
70
  );
48
71
  });
49
72
 
@@ -56,8 +79,8 @@ describe("Gallery", () => {
56
79
  value: [
57
80
  {
58
81
  image: {
59
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
60
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
82
+ path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/cheetah.jpg",
83
+ url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/cheetah.jpg"
61
84
  },
62
85
  caption: null
63
86
  }
@@ -69,8 +92,8 @@ describe("Gallery", () => {
69
92
  value: [
70
93
  {
71
94
  image: {
72
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
73
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
95
+ path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/cheetah.jpg",
96
+ url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/cheetah.jpg"
74
97
  },
75
98
  caption: null
76
99
  }
package/Index.svelte CHANGED
@@ -3,15 +3,17 @@
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
+ import type { GalleryImage, GalleryVideo } from "./types";
6
7
  import type { Gradio, ShareData, SelectData } from "@gradio/utils";
7
8
  import { Block, UploadText } from "@gradio/atoms";
8
9
  import Gallery from "./shared/Gallery.svelte";
9
10
  import type { LoadingStatus } from "@gradio/statustracker";
10
11
  import { StatusTracker } from "@gradio/statustracker";
11
- import type { FileData } from "@gradio/client";
12
12
  import { createEventDispatcher } from "svelte";
13
13
  import { BaseFileUpload } from "@gradio/file";
14
14
 
15
+ type GalleryData = GalleryImage | GalleryVideo;
16
+
15
17
  export let loading_status: LoadingStatus;
16
18
  export let show_label: boolean;
17
19
  export let label: string;
@@ -19,7 +21,8 @@
19
21
  export let elem_id = "";
20
22
  export let elem_classes: string[] = [];
21
23
  export let visible = true;
22
- export let value: { image: FileData; caption: string | null }[] | null = null;
24
+ export let value: GalleryData[] | null = null;
25
+ export let file_types: string[] | null = ["image", "video"];
23
26
  export let container = true;
24
27
  export let scale: number | null = null;
25
28
  export let min_width: number | undefined = undefined;
@@ -47,7 +50,7 @@
47
50
 
48
51
  const dispatch = createEventDispatcher();
49
52
 
50
- $: no_value = Array.isArray(value) ? value.length === 0 : !value;
53
+ $: no_value = value === null ? true : value.length === 0;
51
54
  $: selected_index, dispatch("prop_change", { selected_index });
52
55
  </script>
53
56
 
@@ -76,13 +79,17 @@
76
79
  {label}
77
80
  max_file_size={gradio.max_file_size}
78
81
  file_count={"multiple"}
79
- file_types={["image"]}
82
+ {file_types}
80
83
  i18n={gradio.i18n}
81
- upload={gradio.client.upload}
82
- stream_handler={gradio.client.stream}
84
+ upload={(...args) => gradio.client.upload(...args)}
85
+ stream_handler={(...args) => gradio.client.stream(...args)}
83
86
  on:upload={(e) => {
84
87
  const files = Array.isArray(e.detail) ? e.detail : [e.detail];
85
- value = files.map((x) => ({ image: x, caption: null }));
88
+ value = files.map((x) =>
89
+ x.mime_type?.includes("video")
90
+ ? { video: x, caption: null }
91
+ : { image: x, caption: null }
92
+ );
86
93
  gradio.dispatch("upload", value);
87
94
  }}
88
95
  on:error={({ detail }) => {
@@ -113,7 +120,7 @@
113
120
  {show_share_button}
114
121
  {show_download_button}
115
122
  i18n={gradio.i18n}
116
- _fetch={gradio.client.fetch}
123
+ _fetch={(...args) => gradio.client.fetch(...args)}
117
124
  {show_fullscreen_button}
118
125
  />
119
126
  {/if}
package/dist/Index.svelte CHANGED
@@ -14,6 +14,7 @@ export let elem_id = "";
14
14
  export let elem_classes = [];
15
15
  export let visible = true;
16
16
  export let value = null;
17
+ export let file_types = ["image", "video"];
17
18
  export let container = true;
18
19
  export let scale = null;
19
20
  export let min_width = void 0;
@@ -31,7 +32,7 @@ export let gradio;
31
32
  export let show_fullscreen_button = true;
32
33
  const dispatch = createEventDispatcher();
33
34
  $:
34
- no_value = Array.isArray(value) ? value.length === 0 : !value;
35
+ no_value = value === null ? true : value.length === 0;
35
36
  $:
36
37
  selected_index, dispatch("prop_change", { selected_index });
37
38
  </script>
@@ -61,13 +62,17 @@ $:
61
62
  {label}
62
63
  max_file_size={gradio.max_file_size}
63
64
  file_count={"multiple"}
64
- file_types={["image"]}
65
+ {file_types}
65
66
  i18n={gradio.i18n}
66
- upload={gradio.client.upload}
67
- stream_handler={gradio.client.stream}
67
+ upload={(...args) => gradio.client.upload(...args)}
68
+ stream_handler={(...args) => gradio.client.stream(...args)}
68
69
  on:upload={(e) => {
69
70
  const files = Array.isArray(e.detail) ? e.detail : [e.detail];
70
- value = files.map((x) => ({ image: x, caption: null }));
71
+ value = files.map((x) =>
72
+ x.mime_type?.includes("video")
73
+ ? { video: x, caption: null }
74
+ : { image: x, caption: null }
75
+ );
71
76
  gradio.dispatch("upload", value);
72
77
  }}
73
78
  on:error={({ detail }) => {
@@ -98,7 +103,7 @@ $:
98
103
  {show_share_button}
99
104
  {show_download_button}
100
105
  i18n={gradio.i18n}
101
- _fetch={gradio.client.fetch}
106
+ _fetch={(...args) => gradio.client.fetch(...args)}
102
107
  {show_fullscreen_button}
103
108
  />
104
109
  {/if}
@@ -1,8 +1,8 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  export { default as BaseGallery } from "./shared/Gallery.svelte";
3
+ import type { GalleryImage, GalleryVideo } from "./types";
3
4
  import type { Gradio, ShareData, SelectData } from "@gradio/utils";
4
5
  import type { LoadingStatus } from "@gradio/statustracker";
5
- import type { FileData } from "@gradio/client";
6
6
  declare const __propDef: {
7
7
  props: {
8
8
  loading_status: LoadingStatus;
@@ -12,10 +12,8 @@ declare const __propDef: {
12
12
  elem_id?: string | undefined;
13
13
  elem_classes?: string[] | undefined;
14
14
  visible?: boolean | undefined;
15
- value?: ({
16
- image: FileData;
17
- caption: string | null;
18
- }[] | null) | undefined;
15
+ value?: ((GalleryImage | GalleryVideo)[] | null) | undefined;
16
+ file_types?: (string[] | null) | undefined;
19
17
  container?: boolean | undefined;
20
18
  scale?: (number | null) | undefined;
21
19
  min_width?: number | undefined;
@@ -30,14 +28,8 @@ declare const __propDef: {
30
28
  interactive: boolean;
31
29
  show_download_button?: boolean | undefined;
32
30
  gradio: Gradio<{
33
- change: {
34
- image: FileData;
35
- caption: string | null;
36
- }[] | null;
37
- upload: {
38
- image: FileData;
39
- caption: string | null;
40
- }[] | null;
31
+ change: (GalleryImage | GalleryVideo)[] | null;
32
+ upload: (GalleryImage | GalleryVideo)[] | null;
41
33
  select: SelectData;
42
34
  share: ShareData;
43
35
  error: string;
@@ -1,6 +1,7 @@
1
1
  <script>import { BlockLabel, Empty, ShareButton } from "@gradio/atoms";
2
2
  import { ModifyUpload } from "@gradio/upload";
3
3
  import { Image } from "@gradio/image/shared";
4
+ import { Video } from "@gradio/video/shared";
4
5
  import { dequal } from "dequal";
5
6
  import { createEventDispatcher, onMount } from "svelte";
6
7
  import { tick } from "svelte";
@@ -9,11 +10,12 @@ import {
9
10
  Image as ImageIcon,
10
11
  Maximize,
11
12
  Minimize,
12
- Clear
13
+ Clear,
14
+ Play
13
15
  } from "@gradio/icons";
14
16
  import { FileData } from "@gradio/client";
15
17
  import { format_gallery_for_sharing } from "./utils";
16
- import { IconButton } from "@gradio/atoms";
18
+ import { IconButton, IconButtonWrapper } from "@gradio/atoms";
17
19
  export let show_label = true;
18
20
  export let label;
19
21
  export let value = null;
@@ -39,10 +41,17 @@ $:
39
41
  was_reset = value == null || value.length === 0 ? true : was_reset;
40
42
  let resolved_value = null;
41
43
  $:
42
- resolved_value = value == null ? null : value.map((data) => ({
43
- image: data.image,
44
- caption: data.caption
45
- }));
44
+ resolved_value = value == null ? null : value.map((data) => {
45
+ if ("video" in data) {
46
+ return {
47
+ video: data.video,
48
+ caption: data.caption
49
+ };
50
+ } else if ("image" in data) {
51
+ return { image: data.image, caption: data.caption };
52
+ }
53
+ return {};
54
+ });
46
55
  let prev_value = value;
47
56
  if (selected_index == null && preview && value?.length) {
48
57
  selected_index = 0;
@@ -148,7 +157,7 @@ async function download(file_url, name) {
148
157
  URL.revokeObjectURL(url);
149
158
  }
150
159
  $:
151
- selected_image = selected_index != null && resolved_value != null ? resolved_value[selected_index] : null;
160
+ selected_media = selected_index != null && resolved_value != null ? resolved_value[selected_index] : null;
152
161
  onMount(() => {
153
162
  document.addEventListener("fullscreenchange", () => {
154
163
  is_full_screen = !!document.fullscreenElement;
@@ -172,19 +181,22 @@ const toggle_full_screen = async () => {
172
181
  <Empty unpadded_box={true} size="large"><ImageIcon /></Empty>
173
182
  {:else}
174
183
  <div class="gallery-container" bind:this={gallery_container}>
175
- {#if selected_image && allow_preview}
184
+ {#if selected_media && allow_preview}
176
185
  <button
177
186
  on:keydown={on_keydown}
178
187
  class="preview"
179
188
  class:minimal={mode === "minimal"}
180
189
  >
181
- <div class="icon-buttons">
190
+ <IconButtonWrapper>
182
191
  {#if show_download_button}
183
192
  <IconButton
184
193
  Icon={Download}
185
194
  label={i18n("common.download")}
186
195
  on:click={() => {
187
- const image = selected_image?.image;
196
+ const image =
197
+ "image" in selected_media
198
+ ? selected_media?.image
199
+ : selected_media?.video;
188
200
  if (image == null) {
189
201
  return;
190
202
  }
@@ -221,27 +233,42 @@ const toggle_full_screen = async () => {
221
233
  on:click={() => (selected_index = null)}
222
234
  />
223
235
  {/if}
224
- </div>
236
+ </IconButtonWrapper>
225
237
  <button
226
- class="image-button"
227
- on:click={(event) => handle_preview_click(event)}
228
- style="height: calc(100% - {selected_image.caption
238
+ class="media-button"
239
+ on:click={"image" in selected_media
240
+ ? (event) => handle_preview_click(event)
241
+ : null}
242
+ style="height: calc(100% - {selected_media.caption
229
243
  ? '80px'
230
244
  : '60px'})"
231
245
  aria-label="detailed view of selected image"
232
246
  >
233
- <Image
234
- data-testid="detailed-image"
235
- src={selected_image.image.url}
236
- alt={selected_image.caption || ""}
237
- title={selected_image.caption || null}
238
- class={selected_image.caption && "with-caption"}
239
- loading="lazy"
240
- />
247
+ {#if "image" in selected_media}
248
+ <Image
249
+ data-testid="detailed-image"
250
+ src={selected_media.image.url}
251
+ alt={selected_media.caption || ""}
252
+ title={selected_media.caption || null}
253
+ class={selected_media.caption && "with-caption"}
254
+ loading="lazy"
255
+ />
256
+ {:else}
257
+ <Video
258
+ src={selected_media.video.url}
259
+ data-testid={"detailed-video"}
260
+ alt={selected_media.caption || ""}
261
+ loading="lazy"
262
+ loop={false}
263
+ is_stream={false}
264
+ muted={false}
265
+ controls={true}
266
+ />
267
+ {/if}
241
268
  </button>
242
- {#if selected_image?.caption}
269
+ {#if selected_media?.caption}
243
270
  <caption class="caption">
244
- {selected_image.caption}
271
+ {selected_media.caption}
245
272
  </caption>
246
273
  {/if}
247
274
  <div
@@ -249,7 +276,7 @@ const toggle_full_screen = async () => {
249
276
  class="thumbnails scroll-hide"
250
277
  data-testid="container_el"
251
278
  >
252
- {#each resolved_value as image, i}
279
+ {#each resolved_value as media, i}
253
280
  <button
254
281
  bind:this={el[i]}
255
282
  on:click={() => (selected_index = i)}
@@ -260,13 +287,26 @@ const toggle_full_screen = async () => {
260
287
  " of " +
261
288
  resolved_value.length}
262
289
  >
263
- <Image
264
- src={image.image.url}
265
- title={image.caption || null}
266
- data-testid={"thumbnail " + (i + 1)}
267
- alt=""
268
- loading="lazy"
269
- />
290
+ {#if "image" in media}
291
+ <Image
292
+ src={media.image.url}
293
+ title={media.caption || null}
294
+ data-testid={"thumbnail " + (i + 1)}
295
+ alt=""
296
+ loading="lazy"
297
+ />
298
+ {:else}
299
+ <Play />
300
+ <Video
301
+ src={media.video.url}
302
+ title={media.caption || null}
303
+ is_stream={false}
304
+ data-testid={"thumbnail " + (i + 1)}
305
+ alt=""
306
+ loading="lazy"
307
+ loop={false}
308
+ />
309
+ {/if}
270
310
  </button>
271
311
  {/each}
272
312
  </div>
@@ -286,24 +326,22 @@ const toggle_full_screen = async () => {
286
326
  >
287
327
  {#if interactive}
288
328
  <div class="icon-button">
289
- <ModifyUpload
290
- {i18n}
291
- absolute={false}
292
- on:clear={() => (value = null)}
293
- />
294
- </div>
295
- {/if}
296
- {#if show_share_button}
297
- <div class="icon-button">
298
- <ShareButton
299
- {i18n}
300
- on:share
301
- on:error
302
- value={resolved_value}
303
- formatter={format_gallery_for_sharing}
304
- />
329
+ <ModifyUpload {i18n} on:clear={() => (value = [])} />
305
330
  </div>
306
331
  {/if}
332
+ <IconButtonWrapper>
333
+ {#if show_share_button}
334
+ <div class="icon-button">
335
+ <ShareButton
336
+ {i18n}
337
+ on:share
338
+ on:error
339
+ value={resolved_value}
340
+ formatter={format_gallery_for_sharing}
341
+ />
342
+ </div>
343
+ {/if}
344
+ </IconButtonWrapper>
307
345
  {#each resolved_value as entry, i}
308
346
  <button
309
347
  class="thumbnail-item thumbnail-lg"
@@ -311,13 +349,26 @@ const toggle_full_screen = async () => {
311
349
  on:click={() => (selected_index = i)}
312
350
  aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
313
351
  >
314
- <Image
315
- alt={entry.caption || ""}
316
- src={typeof entry.image === "string"
317
- ? entry.image
318
- : entry.image.url}
319
- loading="lazy"
320
- />
352
+ {#if "image" in entry}
353
+ <Image
354
+ alt={entry.caption || ""}
355
+ src={typeof entry.image === "string"
356
+ ? entry.image
357
+ : entry.image.url}
358
+ loading="lazy"
359
+ />
360
+ {:else}
361
+ <Play />
362
+ <Video
363
+ src={entry.video.url}
364
+ title={entry.caption || null}
365
+ is_stream={false}
366
+ data-testid={"thumbnail " + (i + 1)}
367
+ alt=""
368
+ loading="lazy"
369
+ loop={false}
370
+ />
371
+ {/if}
321
372
  {#if entry.caption}
322
373
  <div class="caption-label">
323
374
  {entry.caption}
@@ -382,12 +433,13 @@ const toggle_full_screen = async () => {
382
433
  }
383
434
  }
384
435
 
385
- .image-button {
436
+ .media-button {
386
437
  height: calc(100% - 60px);
387
438
  width: 100%;
388
439
  display: flex;
389
440
  }
390
- .image-button :global(img) {
441
+ .media-button :global(img),
442
+ .media-button :global(video) {
391
443
  width: var(--size-full);
392
444
  height: var(--size-full);
393
445
  object-fit: contain;
@@ -397,6 +449,14 @@ const toggle_full_screen = async () => {
397
449
  width: var(--size-full);
398
450
  height: var(--size-full);
399
451
  }
452
+ .thumbnails :global(svg) {
453
+ position: absolute;
454
+ top: var(--size-2);
455
+ left: var(--size-2);
456
+ width: 50%;
457
+ height: 50%;
458
+ opacity: 50%;
459
+ }
400
460
  .preview :global(img.with-caption) {
401
461
  height: var(--size-full);
402
462
  }
@@ -458,6 +518,23 @@ const toggle_full_screen = async () => {
458
518
  border-color: var(--color-accent);
459
519
  }
460
520
 
521
+ .thumbnail-item :global(svg) {
522
+ position: absolute;
523
+ top: 50%;
524
+ left: 50%;
525
+ width: 50%;
526
+ height: 50%;
527
+ opacity: 50%;
528
+ transform: translate(-50%, -50%);
529
+ }
530
+
531
+ .thumbnail-item :global(video) {
532
+ width: var(--size-full);
533
+ height: var(--size-full);
534
+ overflow: hidden;
535
+ object-fit: cover;
536
+ }
537
+
461
538
  .thumbnail-small {
462
539
  flex: none;
463
540
  transform: scale(0.9);
@@ -465,7 +542,6 @@ const toggle_full_screen = async () => {
465
542
  width: var(--size-9);
466
543
  height: var(--size-9);
467
544
  }
468
-
469
545
  .thumbnail-small.selected {
470
546
  --ring-color: var(--color-accent);
471
547
  transform: scale(1);
@@ -525,19 +601,8 @@ const toggle_full_screen = async () => {
525
601
  }
526
602
 
527
603
  .icon-button {
528
- position: absolute;
529
- top: 0px;
530
- right: 0px;
531
- z-index: var(--layer-1);
532
- }
533
-
534
- .icon-buttons {
535
- display: flex;
536
- position: absolute;
537
- right: 0;
538
- gap: var(--size-1);
539
- z-index: 1;
540
- margin: var(--size-1);
604
+ top: 1px;
605
+ right: 1px;
541
606
  }
542
607
 
543
608
  .grid-wrap.minimal {
@@ -1,15 +1,12 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  import type { SelectData } from "@gradio/utils";
3
- import { FileData } from "@gradio/client";
3
+ import type { GalleryImage, GalleryVideo } from "../types";
4
4
  import type { I18nFormatter } from "@gradio/utils";
5
5
  declare const __propDef: {
6
6
  props: {
7
7
  show_label?: boolean | undefined;
8
8
  label: string;
9
- value?: ({
10
- image: FileData;
11
- caption: string | null;
12
- }[] | null) | undefined;
9
+ value?: ((GalleryImage | GalleryVideo)[] | null) | undefined;
13
10
  columns?: number | number[] | undefined;
14
11
  rows?: number | number[] | undefined;
15
12
  height?: (number | "auto") | undefined;
@@ -0,0 +1,9 @@
1
+ import type { FileData } from "@gradio/client";
2
+ export interface GalleryImage {
3
+ image: FileData;
4
+ caption: string | null;
5
+ }
6
+ export interface GalleryVideo {
7
+ video: FileData;
8
+ caption: string | null;
9
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/gallery",
3
- "version": "0.12.2",
3
+ "version": "0.13.0-beta.2",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -8,14 +8,15 @@
8
8
  "private": false,
9
9
  "dependencies": {
10
10
  "dequal": "^2.0.2",
11
- "@gradio/atoms": "^0.8.1",
12
- "@gradio/icons": "^0.7.2",
13
- "@gradio/image": "^0.15.1",
14
- "@gradio/statustracker": "^0.7.6",
15
- "@gradio/client": "^1.5.2",
16
- "@gradio/utils": "^0.6.1",
17
- "@gradio/file": "^0.9.4",
18
- "@gradio/upload": "^0.12.4"
11
+ "@gradio/atoms": "^0.9.0-beta.2",
12
+ "@gradio/client": "^1.6.0-beta.2",
13
+ "@gradio/image": "^0.16.0-beta.2",
14
+ "@gradio/icons": "^0.8.0-beta.2",
15
+ "@gradio/statustracker": "^0.8.0-beta.2",
16
+ "@gradio/upload": "^0.13.0-beta.2",
17
+ "@gradio/utils": "^0.7.0-beta.2",
18
+ "@gradio/video": "^0.11.0-beta.2",
19
+ "@gradio/file": "^0.10.0-beta.2"
19
20
  },
20
21
  "devDependencies": {
21
22
  "@gradio/preview": "^0.11.1"
@@ -3,28 +3,30 @@
3
3
  import { ModifyUpload } from "@gradio/upload";
4
4
  import type { SelectData } from "@gradio/utils";
5
5
  import { Image } from "@gradio/image/shared";
6
+ import { Video } from "@gradio/video/shared";
6
7
  import { dequal } from "dequal";
7
8
  import { createEventDispatcher, onMount } from "svelte";
8
9
  import { tick } from "svelte";
10
+ import type { GalleryImage, GalleryVideo } from "../types";
9
11
 
10
12
  import {
11
13
  Download,
12
14
  Image as ImageIcon,
13
15
  Maximize,
14
16
  Minimize,
15
- Clear
17
+ Clear,
18
+ Play
16
19
  } from "@gradio/icons";
17
20
  import { FileData } from "@gradio/client";
18
21
  import { format_gallery_for_sharing } from "./utils";
19
- import { IconButton } from "@gradio/atoms";
22
+ import { IconButton, IconButtonWrapper } from "@gradio/atoms";
20
23
  import type { I18nFormatter } from "@gradio/utils";
21
24
 
22
- type GalleryImage = { image: FileData; caption: string | null };
23
- type GalleryData = GalleryImage[];
25
+ type GalleryData = GalleryImage | GalleryVideo;
24
26
 
25
27
  export let show_label = true;
26
28
  export let label: string;
27
- export let value: GalleryData | null = null;
29
+ export let value: GalleryData[] | null = null;
28
30
  export let columns: number | number[] | undefined = [2];
29
31
  export let rows: number | number[] | undefined = undefined;
30
32
  export let height: number | "auto" = "auto";
@@ -54,16 +56,24 @@
54
56
 
55
57
  $: was_reset = value == null || value.length === 0 ? true : was_reset;
56
58
 
57
- let resolved_value: GalleryData | null = null;
59
+ let resolved_value: GalleryData[] | null = null;
60
+
58
61
  $: resolved_value =
59
62
  value == null
60
63
  ? null
61
- : value.map((data) => ({
62
- image: data.image as FileData,
63
- caption: data.caption
64
- }));
65
-
66
- let prev_value: GalleryData | null = value;
64
+ : (value.map((data) => {
65
+ if ("video" in data) {
66
+ return {
67
+ video: data.video as FileData,
68
+ caption: data.caption
69
+ };
70
+ } else if ("image" in data) {
71
+ return { image: data.image as FileData, caption: data.caption };
72
+ }
73
+ return {};
74
+ }) as GalleryData[]);
75
+
76
+ let prev_value: GalleryData[] | null = value;
67
77
  if (selected_index == null && preview && value?.length) {
68
78
  selected_index = 0;
69
79
  }
@@ -201,7 +211,7 @@
201
211
  URL.revokeObjectURL(url);
202
212
  }
203
213
 
204
- $: selected_image =
214
+ $: selected_media =
205
215
  selected_index != null && resolved_value != null
206
216
  ? resolved_value[selected_index]
207
217
  : null;
@@ -230,19 +240,22 @@
230
240
  <Empty unpadded_box={true} size="large"><ImageIcon /></Empty>
231
241
  {:else}
232
242
  <div class="gallery-container" bind:this={gallery_container}>
233
- {#if selected_image && allow_preview}
243
+ {#if selected_media && allow_preview}
234
244
  <button
235
245
  on:keydown={on_keydown}
236
246
  class="preview"
237
247
  class:minimal={mode === "minimal"}
238
248
  >
239
- <div class="icon-buttons">
249
+ <IconButtonWrapper>
240
250
  {#if show_download_button}
241
251
  <IconButton
242
252
  Icon={Download}
243
253
  label={i18n("common.download")}
244
254
  on:click={() => {
245
- const image = selected_image?.image;
255
+ const image =
256
+ "image" in selected_media
257
+ ? selected_media?.image
258
+ : selected_media?.video;
246
259
  if (image == null) {
247
260
  return;
248
261
  }
@@ -279,27 +292,42 @@
279
292
  on:click={() => (selected_index = null)}
280
293
  />
281
294
  {/if}
282
- </div>
295
+ </IconButtonWrapper>
283
296
  <button
284
- class="image-button"
285
- on:click={(event) => handle_preview_click(event)}
286
- style="height: calc(100% - {selected_image.caption
297
+ class="media-button"
298
+ on:click={"image" in selected_media
299
+ ? (event) => handle_preview_click(event)
300
+ : null}
301
+ style="height: calc(100% - {selected_media.caption
287
302
  ? '80px'
288
303
  : '60px'})"
289
304
  aria-label="detailed view of selected image"
290
305
  >
291
- <Image
292
- data-testid="detailed-image"
293
- src={selected_image.image.url}
294
- alt={selected_image.caption || ""}
295
- title={selected_image.caption || null}
296
- class={selected_image.caption && "with-caption"}
297
- loading="lazy"
298
- />
306
+ {#if "image" in selected_media}
307
+ <Image
308
+ data-testid="detailed-image"
309
+ src={selected_media.image.url}
310
+ alt={selected_media.caption || ""}
311
+ title={selected_media.caption || null}
312
+ class={selected_media.caption && "with-caption"}
313
+ loading="lazy"
314
+ />
315
+ {:else}
316
+ <Video
317
+ src={selected_media.video.url}
318
+ data-testid={"detailed-video"}
319
+ alt={selected_media.caption || ""}
320
+ loading="lazy"
321
+ loop={false}
322
+ is_stream={false}
323
+ muted={false}
324
+ controls={true}
325
+ />
326
+ {/if}
299
327
  </button>
300
- {#if selected_image?.caption}
328
+ {#if selected_media?.caption}
301
329
  <caption class="caption">
302
- {selected_image.caption}
330
+ {selected_media.caption}
303
331
  </caption>
304
332
  {/if}
305
333
  <div
@@ -307,7 +335,7 @@
307
335
  class="thumbnails scroll-hide"
308
336
  data-testid="container_el"
309
337
  >
310
- {#each resolved_value as image, i}
338
+ {#each resolved_value as media, i}
311
339
  <button
312
340
  bind:this={el[i]}
313
341
  on:click={() => (selected_index = i)}
@@ -318,13 +346,26 @@
318
346
  " of " +
319
347
  resolved_value.length}
320
348
  >
321
- <Image
322
- src={image.image.url}
323
- title={image.caption || null}
324
- data-testid={"thumbnail " + (i + 1)}
325
- alt=""
326
- loading="lazy"
327
- />
349
+ {#if "image" in media}
350
+ <Image
351
+ src={media.image.url}
352
+ title={media.caption || null}
353
+ data-testid={"thumbnail " + (i + 1)}
354
+ alt=""
355
+ loading="lazy"
356
+ />
357
+ {:else}
358
+ <Play />
359
+ <Video
360
+ src={media.video.url}
361
+ title={media.caption || null}
362
+ is_stream={false}
363
+ data-testid={"thumbnail " + (i + 1)}
364
+ alt=""
365
+ loading="lazy"
366
+ loop={false}
367
+ />
368
+ {/if}
328
369
  </button>
329
370
  {/each}
330
371
  </div>
@@ -344,24 +385,22 @@
344
385
  >
345
386
  {#if interactive}
346
387
  <div class="icon-button">
347
- <ModifyUpload
348
- {i18n}
349
- absolute={false}
350
- on:clear={() => (value = null)}
351
- />
352
- </div>
353
- {/if}
354
- {#if show_share_button}
355
- <div class="icon-button">
356
- <ShareButton
357
- {i18n}
358
- on:share
359
- on:error
360
- value={resolved_value}
361
- formatter={format_gallery_for_sharing}
362
- />
388
+ <ModifyUpload {i18n} on:clear={() => (value = [])} />
363
389
  </div>
364
390
  {/if}
391
+ <IconButtonWrapper>
392
+ {#if show_share_button}
393
+ <div class="icon-button">
394
+ <ShareButton
395
+ {i18n}
396
+ on:share
397
+ on:error
398
+ value={resolved_value}
399
+ formatter={format_gallery_for_sharing}
400
+ />
401
+ </div>
402
+ {/if}
403
+ </IconButtonWrapper>
365
404
  {#each resolved_value as entry, i}
366
405
  <button
367
406
  class="thumbnail-item thumbnail-lg"
@@ -369,13 +408,26 @@
369
408
  on:click={() => (selected_index = i)}
370
409
  aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
371
410
  >
372
- <Image
373
- alt={entry.caption || ""}
374
- src={typeof entry.image === "string"
375
- ? entry.image
376
- : entry.image.url}
377
- loading="lazy"
378
- />
411
+ {#if "image" in entry}
412
+ <Image
413
+ alt={entry.caption || ""}
414
+ src={typeof entry.image === "string"
415
+ ? entry.image
416
+ : entry.image.url}
417
+ loading="lazy"
418
+ />
419
+ {:else}
420
+ <Play />
421
+ <Video
422
+ src={entry.video.url}
423
+ title={entry.caption || null}
424
+ is_stream={false}
425
+ data-testid={"thumbnail " + (i + 1)}
426
+ alt=""
427
+ loading="lazy"
428
+ loop={false}
429
+ />
430
+ {/if}
379
431
  {#if entry.caption}
380
432
  <div class="caption-label">
381
433
  {entry.caption}
@@ -440,12 +492,13 @@
440
492
  }
441
493
  }
442
494
 
443
- .image-button {
495
+ .media-button {
444
496
  height: calc(100% - 60px);
445
497
  width: 100%;
446
498
  display: flex;
447
499
  }
448
- .image-button :global(img) {
500
+ .media-button :global(img),
501
+ .media-button :global(video) {
449
502
  width: var(--size-full);
450
503
  height: var(--size-full);
451
504
  object-fit: contain;
@@ -455,6 +508,14 @@
455
508
  width: var(--size-full);
456
509
  height: var(--size-full);
457
510
  }
511
+ .thumbnails :global(svg) {
512
+ position: absolute;
513
+ top: var(--size-2);
514
+ left: var(--size-2);
515
+ width: 50%;
516
+ height: 50%;
517
+ opacity: 50%;
518
+ }
458
519
  .preview :global(img.with-caption) {
459
520
  height: var(--size-full);
460
521
  }
@@ -516,6 +577,23 @@
516
577
  border-color: var(--color-accent);
517
578
  }
518
579
 
580
+ .thumbnail-item :global(svg) {
581
+ position: absolute;
582
+ top: 50%;
583
+ left: 50%;
584
+ width: 50%;
585
+ height: 50%;
586
+ opacity: 50%;
587
+ transform: translate(-50%, -50%);
588
+ }
589
+
590
+ .thumbnail-item :global(video) {
591
+ width: var(--size-full);
592
+ height: var(--size-full);
593
+ overflow: hidden;
594
+ object-fit: cover;
595
+ }
596
+
519
597
  .thumbnail-small {
520
598
  flex: none;
521
599
  transform: scale(0.9);
@@ -523,7 +601,6 @@
523
601
  width: var(--size-9);
524
602
  height: var(--size-9);
525
603
  }
526
-
527
604
  .thumbnail-small.selected {
528
605
  --ring-color: var(--color-accent);
529
606
  transform: scale(1);
@@ -583,19 +660,8 @@
583
660
  }
584
661
 
585
662
  .icon-button {
586
- position: absolute;
587
- top: 0px;
588
- right: 0px;
589
- z-index: var(--layer-1);
590
- }
591
-
592
- .icon-buttons {
593
- display: flex;
594
- position: absolute;
595
- right: 0;
596
- gap: var(--size-1);
597
- z-index: 1;
598
- margin: var(--size-1);
663
+ top: 1px;
664
+ right: 1px;
599
665
  }
600
666
 
601
667
  .grid-wrap.minimal {
package/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { FileData } from "@gradio/client";
2
+
3
+ export interface GalleryImage {
4
+ image: FileData;
5
+ caption: string | null;
6
+ }
7
+
8
+ export interface GalleryVideo {
9
+ video: FileData;
10
+ caption: string | null;
11
+ }