@gradio/gallery 0.15.31 → 0.15.33

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,51 @@
1
1
  # @gradio/gallery
2
2
 
3
+ ## 0.15.33
4
+
5
+ ### Fixes
6
+
7
+ - [#11784](https://github.com/gradio-app/gradio/pull/11784) [`d9dd3f5`](https://github.com/gradio-app/gradio/commit/d9dd3f54b7fb34cf7118e549d39fc63937ca3489) - Add "hidden" option to component's `visible` kwarg to render but visually hide the component. Thanks @pngwn!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/statustracker@0.11.1
12
+ - @gradio/atoms@0.18.0
13
+ - @gradio/client@1.19.0
14
+ - @gradio/upload@0.17.0
15
+ - @gradio/image@0.23.0
16
+ - @gradio/file@0.13.0
17
+ - @gradio/video@0.16.0
18
+
19
+ ## 0.15.32
20
+
21
+ ### Fixes
22
+
23
+ - [#11787](https://github.com/gradio-app/gradio/pull/11787) [`b19576c`](https://github.com/gradio-app/gradio/commit/b19576c45f0748ca655864f4d80ff835318f1299) - Allow deleting and uploading additional images to interactive `gr.Gallery` and other fixes. Thanks @abidlabs!
24
+
25
+ ### Dependency updates
26
+
27
+ - @gradio/client@1.18.0
28
+ - @gradio/icons@0.14.0
29
+ - @gradio/atoms@0.17.0
30
+ - @gradio/statustracker@0.11.0
31
+ - @gradio/upload@0.16.17
32
+ - @gradio/file@0.12.29
33
+ - @gradio/image@0.22.18
34
+ - @gradio/video@0.15.1
35
+
36
+ ## 0.15.31
37
+
38
+ ### Dependency updates
39
+
40
+ - @gradio/statustracker@0.10.18
41
+
42
+ ## 0.15.31
43
+
44
+ ### Dependency updates
45
+
46
+ - @gradio/icons@0.13.1
47
+ - @gradio/upload@0.16.16
48
+
3
49
  ## 0.15.31
4
50
 
5
51
  ### Dependency updates
package/Example.svelte ADDED
@@ -0,0 +1,143 @@
1
+ <script lang="ts">
2
+ import type { GalleryImage, GalleryVideo } from "./types";
3
+
4
+ export let value: (GalleryImage | GalleryVideo)[] | null;
5
+ export let type: "gallery" | "table";
6
+ export let selected = false;
7
+ </script>
8
+
9
+ <div
10
+ class="container"
11
+ class:table={type === "table"}
12
+ class:gallery={type === "gallery"}
13
+ class:selected
14
+ >
15
+ {#if value && value.length > 0}
16
+ <div class="images-wrapper">
17
+ {#each value.slice(0, 3) as item}
18
+ {#if "image" in item && item.image}
19
+ <div class="image-container">
20
+ <img src={item.image.url} alt={item.caption || ""} />
21
+ {#if item.caption}
22
+ <span class="caption">{item.caption}</span>
23
+ {/if}
24
+ </div>
25
+ {:else if "video" in item && item.video}
26
+ <div class="image-container">
27
+ <video
28
+ src={item.video.url}
29
+ controls={false}
30
+ muted
31
+ preload="metadata"
32
+ />
33
+ {#if item.caption}
34
+ <span class="caption">{item.caption}</span>
35
+ {/if}
36
+ </div>
37
+ {/if}
38
+ {/each}
39
+ {#if value.length > 3}
40
+ <div class="more-indicator">…</div>
41
+ {/if}
42
+ </div>
43
+ {/if}
44
+ </div>
45
+
46
+ <style>
47
+ .container {
48
+ border-radius: var(--radius-lg);
49
+ overflow: hidden;
50
+ }
51
+
52
+ .container.selected {
53
+ border: 2px solid var(--border-color-accent);
54
+ }
55
+
56
+ .images-wrapper {
57
+ display: flex;
58
+ gap: var(--spacing-sm);
59
+ }
60
+
61
+ .container.table .images-wrapper {
62
+ flex-direction: row;
63
+ align-items: center;
64
+ padding: var(--spacing-sm);
65
+ border: 1px solid var(--border-color-primary);
66
+ border-radius: var(--radius-lg);
67
+ background: var(--background-fill-secondary);
68
+ }
69
+
70
+ .container.gallery .images-wrapper {
71
+ flex-direction: row;
72
+ gap: 0;
73
+ }
74
+
75
+ .image-container {
76
+ position: relative;
77
+ flex-shrink: 0;
78
+ }
79
+
80
+ .container.table .image-container {
81
+ width: var(--size-12);
82
+ height: var(--size-12);
83
+ }
84
+
85
+ .container.gallery .image-container {
86
+ width: var(--size-20);
87
+ height: var(--size-20);
88
+ margin-left: calc(-1 * var(--size-8));
89
+ }
90
+
91
+ .container.gallery .image-container:first-child {
92
+ margin-left: 0;
93
+ }
94
+
95
+ .more-indicator {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ font-size: var(--text-lg);
100
+ font-weight: bold;
101
+ color: var(--border-color-primary);
102
+ }
103
+
104
+ .container.table .more-indicator {
105
+ width: var(--size-12);
106
+ height: var(--size-12);
107
+ }
108
+
109
+ .container.gallery .more-indicator {
110
+ width: var(--size-20);
111
+ height: var(--size-20);
112
+ margin-left: calc(-1 * var(--size-8));
113
+ margin-right: calc(-1 * var(--size-6));
114
+ }
115
+
116
+ .image-container img,
117
+ .image-container video {
118
+ width: 100%;
119
+ height: 100%;
120
+ object-fit: cover;
121
+ border-radius: var(--radius-md);
122
+ }
123
+
124
+ .caption {
125
+ position: absolute;
126
+ bottom: 0;
127
+ left: 0;
128
+ right: 0;
129
+ background: rgba(0, 0, 0, 0.7);
130
+ color: white;
131
+ padding: var(--spacing-xs);
132
+ font-size: var(--text-xs);
133
+ text-align: center;
134
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
135
+ overflow: hidden;
136
+ text-overflow: ellipsis;
137
+ white-space: nowrap;
138
+ }
139
+
140
+ .container.table .caption {
141
+ display: none;
142
+ }
143
+ </style>
package/Index.svelte CHANGED
@@ -1,5 +1,6 @@
1
1
  <script context="module" lang="ts">
2
2
  export { default as BaseGallery } from "./shared/Gallery.svelte";
3
+ export { default as BaseExample } from "./Example.svelte";
3
4
  </script>
4
5
 
5
6
  <script lang="ts">
@@ -21,7 +22,7 @@
21
22
  export let root: string;
22
23
  export let elem_id = "";
23
24
  export let elem_classes: string[] = [];
24
- export let visible = true;
25
+ export let visible: boolean | "hidden" = true;
25
26
  export let value: GalleryData[] | null = null;
26
27
  export let file_types: string[] | null = ["image", "video"];
27
28
  export let container = true;
@@ -44,6 +45,7 @@
44
45
  select: SelectData;
45
46
  share: ShareData;
46
47
  error: string;
48
+ delete: { file: FileData; index: number };
47
49
  prop_change: Record<string, any>;
48
50
  clear_status: LoadingStatus;
49
51
  preview_open: never;
@@ -55,6 +57,16 @@
55
57
  const dispatch = createEventDispatcher();
56
58
 
57
59
  $: no_value = value === null ? true : value.length === 0;
60
+
61
+ function handle_delete(
62
+ event: CustomEvent<{ file: FileData; index: number }>
63
+ ): void {
64
+ if (!value) return;
65
+ const { index } = event.detail;
66
+ gradio.dispatch("delete", event.detail);
67
+ value = value.filter((_, i) => i !== index);
68
+ gradio.dispatch("change", value);
69
+ }
58
70
  $: selected_index, dispatch("prop_change", { selected_index });
59
71
 
60
72
  async function process_upload_files(
@@ -137,6 +149,14 @@
137
149
  on:fullscreen={({ detail }) => {
138
150
  fullscreen = detail;
139
151
  }}
152
+ on:delete={handle_delete}
153
+ on:upload={async (e) => {
154
+ const files = Array.isArray(e.detail) ? e.detail : [e.detail];
155
+ const new_value = await process_upload_files(files);
156
+ value = value ? [...value, ...new_value] : new_value;
157
+ gradio.dispatch("upload", new_value);
158
+ gradio.dispatch("change", value);
159
+ }}
140
160
  {label}
141
161
  {show_label}
142
162
  {columns}
@@ -154,6 +174,11 @@
154
174
  _fetch={(...args) => gradio.client.fetch(...args)}
155
175
  {show_fullscreen_button}
156
176
  {fullscreen}
177
+ {root}
178
+ {file_types}
179
+ max_file_size={gradio.max_file_size}
180
+ upload={(...args) => gradio.client.upload(...args)}
181
+ stream_handler={(...args) => gradio.client.stream(...args)}
157
182
  />
158
183
  {/if}
159
184
  </Block>
@@ -0,0 +1,140 @@
1
+ <script>export let value;
2
+ export let type;
3
+ export let selected = false;
4
+ </script>
5
+
6
+ <div
7
+ class="container"
8
+ class:table={type === "table"}
9
+ class:gallery={type === "gallery"}
10
+ class:selected
11
+ >
12
+ {#if value && value.length > 0}
13
+ <div class="images-wrapper">
14
+ {#each value.slice(0, 3) as item}
15
+ {#if "image" in item && item.image}
16
+ <div class="image-container">
17
+ <img src={item.image.url} alt={item.caption || ""} />
18
+ {#if item.caption}
19
+ <span class="caption">{item.caption}</span>
20
+ {/if}
21
+ </div>
22
+ {:else if "video" in item && item.video}
23
+ <div class="image-container">
24
+ <video
25
+ src={item.video.url}
26
+ controls={false}
27
+ muted
28
+ preload="metadata"
29
+ />
30
+ {#if item.caption}
31
+ <span class="caption">{item.caption}</span>
32
+ {/if}
33
+ </div>
34
+ {/if}
35
+ {/each}
36
+ {#if value.length > 3}
37
+ <div class="more-indicator">…</div>
38
+ {/if}
39
+ </div>
40
+ {/if}
41
+ </div>
42
+
43
+ <style>
44
+ .container {
45
+ border-radius: var(--radius-lg);
46
+ overflow: hidden;
47
+ }
48
+
49
+ .container.selected {
50
+ border: 2px solid var(--border-color-accent);
51
+ }
52
+
53
+ .images-wrapper {
54
+ display: flex;
55
+ gap: var(--spacing-sm);
56
+ }
57
+
58
+ .container.table .images-wrapper {
59
+ flex-direction: row;
60
+ align-items: center;
61
+ padding: var(--spacing-sm);
62
+ border: 1px solid var(--border-color-primary);
63
+ border-radius: var(--radius-lg);
64
+ background: var(--background-fill-secondary);
65
+ }
66
+
67
+ .container.gallery .images-wrapper {
68
+ flex-direction: row;
69
+ gap: 0;
70
+ }
71
+
72
+ .image-container {
73
+ position: relative;
74
+ flex-shrink: 0;
75
+ }
76
+
77
+ .container.table .image-container {
78
+ width: var(--size-12);
79
+ height: var(--size-12);
80
+ }
81
+
82
+ .container.gallery .image-container {
83
+ width: var(--size-20);
84
+ height: var(--size-20);
85
+ margin-left: calc(-1 * var(--size-8));
86
+ }
87
+
88
+ .container.gallery .image-container:first-child {
89
+ margin-left: 0;
90
+ }
91
+
92
+ .more-indicator {
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ font-size: var(--text-lg);
97
+ font-weight: bold;
98
+ color: var(--border-color-primary);
99
+ }
100
+
101
+ .container.table .more-indicator {
102
+ width: var(--size-12);
103
+ height: var(--size-12);
104
+ }
105
+
106
+ .container.gallery .more-indicator {
107
+ width: var(--size-20);
108
+ height: var(--size-20);
109
+ margin-left: calc(-1 * var(--size-8));
110
+ margin-right: calc(-1 * var(--size-6));
111
+ }
112
+
113
+ .image-container img,
114
+ .image-container video {
115
+ width: 100%;
116
+ height: 100%;
117
+ object-fit: cover;
118
+ border-radius: var(--radius-md);
119
+ }
120
+
121
+ .caption {
122
+ position: absolute;
123
+ bottom: 0;
124
+ left: 0;
125
+ right: 0;
126
+ background: rgba(0, 0, 0, 0.7);
127
+ color: white;
128
+ padding: var(--spacing-xs);
129
+ font-size: var(--text-xs);
130
+ text-align: center;
131
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
132
+ overflow: hidden;
133
+ text-overflow: ellipsis;
134
+ white-space: nowrap;
135
+ }
136
+
137
+ .container.table .caption {
138
+ display: none;
139
+ }
140
+ </style>
@@ -0,0 +1,21 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { GalleryImage, GalleryVideo } from "./types";
3
+ declare const __propDef: {
4
+ props: {
5
+ value: (GalleryImage | GalleryVideo)[] | null;
6
+ type: "gallery" | "table";
7
+ selected?: boolean;
8
+ };
9
+ events: {
10
+ [evt: string]: CustomEvent<any>;
11
+ };
12
+ slots: {};
13
+ exports?: {} | undefined;
14
+ bindings?: string | undefined;
15
+ };
16
+ export type ExampleProps = typeof __propDef.props;
17
+ export type ExampleEvents = typeof __propDef.events;
18
+ export type ExampleSlots = typeof __propDef.slots;
19
+ export default class Example extends SvelteComponent<ExampleProps, ExampleEvents, ExampleSlots> {
20
+ }
21
+ export {};
package/dist/Index.svelte CHANGED
@@ -1,4 +1,5 @@
1
1
  <script context="module">export { default as BaseGallery } from "./shared/Gallery.svelte";
2
+ export { default as BaseExample } from "./Example.svelte";
2
3
  </script>
3
4
 
4
5
  <script>import { Block, UploadText } from "@gradio/atoms";
@@ -32,10 +33,15 @@ export let gradio;
32
33
  export let show_fullscreen_button = true;
33
34
  export let fullscreen = false;
34
35
  const dispatch = createEventDispatcher();
35
- $:
36
- no_value = value === null ? true : value.length === 0;
37
- $:
38
- selected_index, dispatch("prop_change", { selected_index });
36
+ $: no_value = value === null ? true : value.length === 0;
37
+ function handle_delete(event) {
38
+ if (!value) return;
39
+ const { index } = event.detail;
40
+ gradio.dispatch("delete", event.detail);
41
+ value = value.filter((_, i) => i !== index);
42
+ gradio.dispatch("change", value);
43
+ }
44
+ $: selected_index, dispatch("prop_change", { selected_index });
39
45
  async function process_upload_files(files) {
40
46
  const processed_files = await Promise.all(
41
47
  files.map(async (x) => {
@@ -111,6 +117,14 @@ async function process_upload_files(files) {
111
117
  on:fullscreen={({ detail }) => {
112
118
  fullscreen = detail;
113
119
  }}
120
+ on:delete={handle_delete}
121
+ on:upload={async (e) => {
122
+ const files = Array.isArray(e.detail) ? e.detail : [e.detail];
123
+ const new_value = await process_upload_files(files);
124
+ value = value ? [...value, ...new_value] : new_value;
125
+ gradio.dispatch("upload", new_value);
126
+ gradio.dispatch("change", value);
127
+ }}
114
128
  {label}
115
129
  {show_label}
116
130
  {columns}
@@ -128,6 +142,11 @@ async function process_upload_files(files) {
128
142
  _fetch={(...args) => gradio.client.fetch(...args)}
129
143
  {show_fullscreen_button}
130
144
  {fullscreen}
145
+ {root}
146
+ {file_types}
147
+ max_file_size={gradio.max_file_size}
148
+ upload={(...args) => gradio.client.upload(...args)}
149
+ stream_handler={(...args) => gradio.client.stream(...args)}
131
150
  />
132
151
  {/if}
133
152
  </Block>
@@ -1,6 +1,8 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  export { default as BaseGallery } from "./shared/Gallery.svelte";
3
+ export { default as BaseExample } from "./Example.svelte";
3
4
  import type { GalleryImage, GalleryVideo } from "./types";
5
+ import type { FileData } from "@gradio/client";
4
6
  import type { Gradio, ShareData, SelectData } from "@gradio/utils";
5
7
  import type { LoadingStatus } from "@gradio/statustracker";
6
8
  declare const __propDef: {
@@ -9,37 +11,41 @@ declare const __propDef: {
9
11
  show_label: boolean;
10
12
  label: string;
11
13
  root: string;
12
- elem_id?: string | undefined;
13
- elem_classes?: string[] | undefined;
14
- visible?: boolean | undefined;
15
- value?: ((GalleryImage | GalleryVideo)[] | null) | undefined;
16
- file_types?: (string[] | null) | undefined;
17
- container?: boolean | undefined;
18
- scale?: (number | null) | undefined;
14
+ elem_id?: string;
15
+ elem_classes?: string[];
16
+ visible?: boolean | "hidden";
17
+ value?: (GalleryImage | GalleryVideo)[] | null;
18
+ file_types?: string[] | null;
19
+ container?: boolean;
20
+ scale?: number | null;
19
21
  min_width?: number | undefined;
20
22
  columns?: number | number[] | undefined;
21
23
  rows?: number | number[] | undefined;
22
- height?: (number | "auto") | undefined;
24
+ height?: number | "auto";
23
25
  preview: boolean;
24
- allow_preview?: boolean | undefined;
25
- selected_index?: (number | null) | undefined;
26
- object_fit?: ("contain" | "cover" | "fill" | "none" | "scale-down") | undefined;
27
- show_share_button?: boolean | undefined;
26
+ allow_preview?: boolean;
27
+ selected_index?: number | null;
28
+ object_fit?: "contain" | "cover" | "fill" | "none" | "scale-down";
29
+ show_share_button?: boolean;
28
30
  interactive: boolean;
29
- show_download_button?: boolean | undefined;
31
+ show_download_button?: boolean;
30
32
  gradio: Gradio<{
31
33
  change: (GalleryImage | GalleryVideo)[] | null;
32
34
  upload: (GalleryImage | GalleryVideo)[] | null;
33
35
  select: SelectData;
34
36
  share: ShareData;
35
37
  error: string;
38
+ delete: {
39
+ file: FileData;
40
+ index: number;
41
+ };
36
42
  prop_change: Record<string, any>;
37
43
  clear_status: LoadingStatus;
38
44
  preview_open: never;
39
45
  preview_close: never;
40
46
  }>;
41
- show_fullscreen_button?: boolean | undefined;
42
- fullscreen?: boolean | undefined;
47
+ show_fullscreen_button?: boolean;
48
+ fullscreen?: boolean;
43
49
  };
44
50
  events: {
45
51
  prop_change: CustomEvent<any>;
@@ -47,6 +53,8 @@ declare const __propDef: {
47
53
  [evt: string]: CustomEvent<any>;
48
54
  };
49
55
  slots: {};
56
+ exports?: {} | undefined;
57
+ bindings?: string | undefined;
50
58
  };
51
59
  export type IndexProps = typeof __propDef.props;
52
60
  export type IndexEvents = typeof __propDef.events;
@@ -6,13 +6,19 @@
6
6
  IconButtonWrapper,
7
7
  FullscreenButton
8
8
  } from "@gradio/atoms";
9
- import { ModifyUpload } from "@gradio/upload";
9
+ import { ModifyUpload, Upload as UploadComponent } from "@gradio/upload";
10
10
  import { Image } from "@gradio/image/shared";
11
11
  import { Video } from "@gradio/video/shared";
12
12
  import { dequal } from "dequal";
13
13
  import { createEventDispatcher, onMount } from "svelte";
14
14
  import { tick } from "svelte";
15
- import { Download, Image as ImageIcon, Clear, Play } from "@gradio/icons";
15
+ import {
16
+ Download,
17
+ Image as ImageIcon,
18
+ Clear,
19
+ Play,
20
+ Upload as UploadIcon
21
+ } from "@gradio/icons";
16
22
  import { FileData } from "@gradio/client";
17
23
  import { format_gallery_for_sharing } from "./utils";
18
24
  export let show_label = true;
@@ -34,52 +40,65 @@ export let mode = "normal";
34
40
  export let show_fullscreen_button = true;
35
41
  export let display_icon_button_wrapper_top_corner = false;
36
42
  export let fullscreen = false;
43
+ export let root = "";
44
+ export let file_types = ["image", "video"];
45
+ export let max_file_size = null;
46
+ export let upload = void 0;
47
+ export let stream_handler = void 0;
37
48
  let is_full_screen = false;
38
49
  let image_container;
39
50
  const dispatch = createEventDispatcher();
40
51
  let was_reset = true;
41
- $:
42
- was_reset = value == null || value.length === 0 ? true : was_reset;
52
+ $: was_reset = value == null || value.length === 0 ? true : was_reset;
43
53
  let resolved_value = null;
44
- $:
45
- resolved_value = value == null ? null : value.map((data) => {
46
- if ("video" in data) {
47
- return {
48
- video: data.video,
49
- caption: data.caption
50
- };
51
- } else if ("image" in data) {
52
- return { image: data.image, caption: data.caption };
54
+ $: resolved_value = value == null ? null : value.map((data) => {
55
+ if ("video" in data) {
56
+ return {
57
+ video: data.video,
58
+ caption: data.caption
59
+ };
60
+ } else if ("image" in data) {
61
+ return { image: data.image, caption: data.caption };
62
+ }
63
+ return {};
64
+ });
65
+ let effective_columns = columns;
66
+ $: {
67
+ if (resolved_value && columns) {
68
+ const item_count = resolved_value.length;
69
+ if (Array.isArray(columns)) {
70
+ effective_columns = columns.map((col) => Math.min(col, item_count));
71
+ } else {
72
+ effective_columns = Math.min(columns, item_count);
53
73
  }
54
- return {};
55
- });
74
+ } else {
75
+ effective_columns = columns;
76
+ }
77
+ }
56
78
  let prev_value = value;
57
79
  if (selected_index == null && preview && value?.length) {
58
80
  selected_index = 0;
59
81
  }
60
82
  let old_selected_index = selected_index;
61
- $:
62
- if (!dequal(prev_value, value)) {
63
- if (was_reset) {
64
- selected_index = preview && value?.length ? 0 : null;
65
- was_reset = false;
83
+ $: if (!dequal(prev_value, value)) {
84
+ if (was_reset) {
85
+ selected_index = preview && value?.length ? 0 : null;
86
+ was_reset = false;
87
+ } else {
88
+ if (selected_index !== null && value !== null) {
89
+ selected_index = Math.max(
90
+ 0,
91
+ Math.min(selected_index, value.length - 1)
92
+ );
66
93
  } else {
67
- if (selected_index !== null && value !== null) {
68
- selected_index = Math.max(
69
- 0,
70
- Math.min(selected_index, value.length - 1)
71
- );
72
- } else {
73
- selected_index = null;
74
- }
94
+ selected_index = null;
75
95
  }
76
- dispatch("change");
77
- prev_value = value;
78
96
  }
79
- $:
80
- previous = ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) % (resolved_value?.length ?? 0);
81
- $:
82
- next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
97
+ dispatch("change");
98
+ prev_value = value;
99
+ }
100
+ $: previous = ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) % (resolved_value?.length ?? 0);
101
+ $: next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
83
102
  function handle_preview_click(event) {
84
103
  const element = event.target;
85
104
  const x = event.offsetX;
@@ -126,18 +145,15 @@ $: {
126
145
  }
127
146
  }
128
147
  }
129
- $:
130
- if (allow_preview) {
131
- scroll_to_img(selected_index);
132
- }
148
+ $: if (allow_preview) {
149
+ scroll_to_img(selected_index);
150
+ }
133
151
  let el = [];
134
152
  let container_element;
135
153
  async function scroll_to_img(index) {
136
- if (typeof index !== "number")
137
- return;
154
+ if (typeof index !== "number") return;
138
155
  await tick();
139
- if (el[index] === void 0)
140
- return;
156
+ if (el[index] === void 0) return;
141
157
  el[index]?.focus();
142
158
  const { left: container_left, width: container_width } = container_element.getBoundingClientRect();
143
159
  const { left, width } = el[index].getBoundingClientRect();
@@ -170,8 +186,7 @@ async function download(file_url, name) {
170
186
  link.click();
171
187
  URL.revokeObjectURL(url);
172
188
  }
173
- $:
174
- selected_media = selected_index != null && resolved_value != null ? resolved_value[selected_index] : null;
189
+ $: selected_media = selected_index != null && resolved_value != null ? resolved_value[selected_index] : null;
175
190
  let thumbnails_overflow = false;
176
191
  function check_thumbnails_overflow() {
177
192
  if (container_element) {
@@ -186,12 +201,30 @@ onMount(() => {
186
201
  window.addEventListener("resize", check_thumbnails_overflow);
187
202
  return () => window.removeEventListener("resize", check_thumbnails_overflow);
188
203
  });
189
- $:
190
- resolved_value, check_thumbnails_overflow();
191
- $:
192
- if (container_element) {
193
- check_thumbnails_overflow();
204
+ $: resolved_value, check_thumbnails_overflow();
205
+ $: if (container_element) {
206
+ check_thumbnails_overflow();
207
+ }
208
+ function handle_item_delete(index) {
209
+ if (!value || !resolved_value) return;
210
+ const deleted_item = resolved_value[index];
211
+ let deleted_file_data;
212
+ if ("image" in deleted_item) {
213
+ deleted_file_data = {
214
+ file: deleted_item.image,
215
+ index
216
+ };
217
+ } else if ("video" in deleted_item) {
218
+ deleted_file_data = {
219
+ file: deleted_item.video,
220
+ index
221
+ };
194
222
  }
223
+ if (deleted_file_data) {
224
+ dispatch("delete", deleted_file_data);
225
+ }
226
+ }
227
+ let uploading = false;
195
228
  </script>
196
229
 
197
230
  <svelte:window bind:innerHeight={window_height} />
@@ -347,51 +380,82 @@ $:
347
380
  class:hidden={is_full_screen}
348
381
  >
349
382
  {#if interactive && selected_index === null}
350
- <ModifyUpload {i18n} on:clear={() => (value = [])} />
383
+ <ModifyUpload {i18n} on:clear={() => (value = [])}>
384
+ {#if upload && stream_handler}
385
+ <IconButton Icon={UploadIcon} label={i18n("common.upload")}>
386
+ <UploadComponent
387
+ icon_upload={true}
388
+ on:load={(e) => dispatch("upload", e.detail)}
389
+ filetype={file_types}
390
+ file_count="multiple"
391
+ {max_file_size}
392
+ {root}
393
+ bind:uploading
394
+ on:error={(e) => dispatch("error", e.detail)}
395
+ {stream_handler}
396
+ {upload}
397
+ />
398
+ </IconButton>
399
+ {/if}
400
+ </ModifyUpload>
351
401
  {/if}
352
402
  <div
353
403
  class="grid-container"
354
- style="--grid-cols:{columns}; --grid-rows:{rows}; --object-fit: {object_fit}; height: {height};"
404
+ style="--grid-cols:{effective_columns}; --grid-rows:{rows}; --object-fit: {object_fit}; height: {height};"
355
405
  class:pt-6={show_label}
356
406
  >
357
407
  {#each resolved_value as entry, i}
358
- <button
359
- class="thumbnail-item thumbnail-lg"
360
- class:selected={selected_index === i}
361
- on:click={() => {
362
- if (selected_index === null && allow_preview) {
363
- dispatch("preview_open");
364
- }
365
- selected_index = i;
366
- }}
367
- aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
368
- >
369
- {#if "image" in entry}
370
- <Image
371
- alt={entry.caption || ""}
372
- src={typeof entry.image === "string"
373
- ? entry.image
374
- : entry.image.url}
375
- loading="lazy"
376
- />
377
- {:else}
378
- <Play />
379
- <Video
380
- src={entry.video.url}
381
- title={entry.caption || null}
382
- is_stream={false}
383
- data-testid={"thumbnail " + (i + 1)}
384
- alt=""
385
- loading="lazy"
386
- loop={false}
387
- />
388
- {/if}
389
- {#if entry.caption}
390
- <div class="caption-label">
391
- {entry.caption}
392
- </div>
408
+ <div class="gallery-item">
409
+ <button
410
+ class="thumbnail-item thumbnail-lg"
411
+ class:selected={selected_index === i}
412
+ on:click={() => {
413
+ if (selected_index === null && allow_preview) {
414
+ dispatch("preview_open");
415
+ }
416
+ selected_index = i;
417
+ }}
418
+ aria-label={"Thumbnail " +
419
+ (i + 1) +
420
+ " of " +
421
+ resolved_value.length}
422
+ >
423
+ {#if "image" in entry}
424
+ <Image
425
+ alt={entry.caption || ""}
426
+ src={typeof entry.image === "string"
427
+ ? entry.image
428
+ : entry.image.url}
429
+ loading="lazy"
430
+ />
431
+ {:else}
432
+ <Play />
433
+ <Video
434
+ src={entry.video.url}
435
+ title={entry.caption || null}
436
+ is_stream={false}
437
+ data-testid={"thumbnail " + (i + 1)}
438
+ alt=""
439
+ loading="lazy"
440
+ loop={false}
441
+ />
442
+ {/if}
443
+ {#if entry.caption}
444
+ <div class="caption-label">
445
+ {entry.caption}
446
+ </div>
447
+ {/if}
448
+ </button>
449
+ {#if interactive}
450
+ <button
451
+ class="delete-button"
452
+ on:click|stopPropagation={() => handle_item_delete(i)}
453
+ aria-label="Delete image"
454
+ >
455
+ <Clear />
456
+ </button>
393
457
  {/if}
394
- </button>
458
+ </div>
395
459
  {/each}
396
460
  </div>
397
461
  </div>
@@ -620,4 +684,49 @@ $:
620
684
  .grid-wrap.minimal {
621
685
  padding: 0;
622
686
  }
687
+
688
+ .gallery-item {
689
+ position: relative;
690
+ width: 100%;
691
+ height: 100%;
692
+ }
693
+
694
+ .delete-button {
695
+ position: absolute;
696
+ bottom: 0;
697
+ left: 0;
698
+ z-index: var(--layer-1);
699
+ border-top: 1px solid var(--border-color-primary);
700
+ border-right: 1px solid var(--border-color-primary);
701
+ border-radius: 0 var(--radius-sm) 0 var(--radius-sm);
702
+ background: var(--background-fill-secondary);
703
+ padding: var(--block-label-padding);
704
+ cursor: pointer;
705
+ display: flex;
706
+ align-items: center;
707
+ justify-content: center;
708
+ opacity: 0;
709
+ transition: opacity 0.2s ease;
710
+ font-size: var(--block-label-text-size);
711
+ color: var(--block-label-text-color);
712
+ font-weight: var(--weight-semibold);
713
+ width: auto;
714
+ height: auto;
715
+ min-width: fit-content;
716
+ min-height: fit-content;
717
+ }
718
+
719
+ .gallery-item:hover .delete-button {
720
+ opacity: 1;
721
+ }
722
+
723
+ .delete-button:hover {
724
+ opacity: 0.8;
725
+ }
726
+
727
+ .delete-button :global(svg) {
728
+ width: var(--text-xs);
729
+ height: var(--text-md);
730
+ color: var(--block-label-text-color);
731
+ }
623
732
  </style>
@@ -1,41 +1,55 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  import type { SelectData } from "@gradio/utils";
3
3
  import type { GalleryImage, GalleryVideo } from "../types";
4
+ import { FileData } from "@gradio/client";
5
+ import type { Client } from "@gradio/client";
4
6
  import type { I18nFormatter } from "@gradio/utils";
5
7
  declare const __propDef: {
6
8
  props: {
7
- show_label?: boolean | undefined;
9
+ show_label?: boolean;
8
10
  label: string;
9
- value?: ((GalleryImage | GalleryVideo)[] | null) | undefined;
11
+ value?: (GalleryImage | GalleryVideo)[] | null;
10
12
  columns?: number | number[] | undefined;
11
13
  rows?: number | number[] | undefined;
12
- height?: (number | "auto") | undefined;
14
+ height?: number | "auto";
13
15
  preview: boolean;
14
- allow_preview?: boolean | undefined;
15
- object_fit?: ("contain" | "cover" | "fill" | "none" | "scale-down") | undefined;
16
- show_share_button?: boolean | undefined;
17
- show_download_button?: boolean | undefined;
16
+ allow_preview?: boolean;
17
+ object_fit?: "contain" | "cover" | "fill" | "none" | "scale-down";
18
+ show_share_button?: boolean;
19
+ show_download_button?: boolean;
18
20
  i18n: I18nFormatter;
19
- selected_index?: (number | null) | undefined;
21
+ selected_index?: number | null;
20
22
  interactive: boolean;
21
23
  _fetch: typeof fetch;
22
- mode?: ("normal" | "minimal") | undefined;
23
- show_fullscreen_button?: boolean | undefined;
24
- display_icon_button_wrapper_top_corner?: boolean | undefined;
25
- fullscreen?: boolean | undefined;
24
+ mode?: "normal" | "minimal";
25
+ show_fullscreen_button?: boolean;
26
+ display_icon_button_wrapper_top_corner?: boolean;
27
+ fullscreen?: boolean;
28
+ root?: string;
29
+ file_types?: string[] | null;
30
+ max_file_size?: number | null;
31
+ upload?: Client["upload"] | undefined;
32
+ stream_handler?: Client["stream"] | undefined;
26
33
  };
27
34
  events: {
28
35
  fullscreen: CustomEvent<any>;
29
36
  share: CustomEvent<import("@gradio/utils").ShareData>;
30
- error: CustomEvent<string>;
37
+ error: CustomEvent<any>;
31
38
  change: CustomEvent<undefined>;
32
39
  select: CustomEvent<SelectData>;
33
40
  preview_open: CustomEvent<undefined>;
34
41
  preview_close: CustomEvent<undefined>;
42
+ delete: CustomEvent<{
43
+ file: FileData;
44
+ index: number;
45
+ }>;
46
+ upload: CustomEvent<any>;
35
47
  } & {
36
48
  [evt: string]: CustomEvent<any>;
37
49
  };
38
50
  slots: {};
51
+ exports?: {} | undefined;
52
+ bindings?: string | undefined;
39
53
  };
40
54
  export type GalleryProps = typeof __propDef.props;
41
55
  export type GalleryEvents = typeof __propDef.events;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/gallery",
3
- "version": "0.15.31",
3
+ "version": "0.15.33",
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.2",
11
- "@gradio/icons": "^0.13.0",
12
- "@gradio/client": "^1.17.1",
13
- "@gradio/atoms": "^0.16.5",
14
- "@gradio/image": "^0.22.17",
15
- "@gradio/upload": "^0.16.15",
11
+ "@gradio/client": "^1.19.0",
12
+ "@gradio/atoms": "^0.18.0",
13
+ "@gradio/icons": "^0.14.0",
14
+ "@gradio/image": "^0.23.0",
15
+ "@gradio/upload": "^0.17.0",
16
16
  "@gradio/utils": "^0.10.2",
17
- "@gradio/video": "^0.15.0",
18
- "@gradio/file": "^0.12.28",
19
- "@gradio/statustracker": "^0.10.17"
17
+ "@gradio/statustracker": "^0.11.1",
18
+ "@gradio/video": "^0.16.0",
19
+ "@gradio/file": "^0.13.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@gradio/preview": "^0.14.0"
@@ -34,6 +34,11 @@
34
34
  "gradio": "./shared/Gallery.svelte",
35
35
  "svelte": "./dist/shared/Gallery.svelte",
36
36
  "types": "./dist/shared/Gallery.svelte.d.ts"
37
+ },
38
+ "./example": {
39
+ "gradio": "./Example.svelte",
40
+ "svelte": "./dist/Example.svelte",
41
+ "types": "./dist/Example.svelte.d.ts"
37
42
  }
38
43
  },
39
44
  "peerDependencies": {
@@ -7,7 +7,7 @@
7
7
  IconButtonWrapper,
8
8
  FullscreenButton
9
9
  } from "@gradio/atoms";
10
- import { ModifyUpload } from "@gradio/upload";
10
+ import { ModifyUpload, Upload as UploadComponent } from "@gradio/upload";
11
11
  import type { SelectData } from "@gradio/utils";
12
12
  import { Image } from "@gradio/image/shared";
13
13
  import { Video } from "@gradio/video/shared";
@@ -16,8 +16,15 @@
16
16
  import { tick } from "svelte";
17
17
  import type { GalleryImage, GalleryVideo } from "../types";
18
18
 
19
- import { Download, Image as ImageIcon, Clear, Play } from "@gradio/icons";
19
+ import {
20
+ Download,
21
+ Image as ImageIcon,
22
+ Clear,
23
+ Play,
24
+ Upload as UploadIcon
25
+ } from "@gradio/icons";
20
26
  import { FileData } from "@gradio/client";
27
+ import type { Client } from "@gradio/client";
21
28
  import { format_gallery_for_sharing } from "./utils";
22
29
  import type { I18nFormatter } from "@gradio/utils";
23
30
 
@@ -43,6 +50,11 @@
43
50
  export let show_fullscreen_button = true;
44
51
  export let display_icon_button_wrapper_top_corner = false;
45
52
  export let fullscreen = false;
53
+ export let root = "";
54
+ export let file_types: string[] | null = ["image", "video"];
55
+ export let max_file_size: number | null = null;
56
+ export let upload: Client["upload"] | undefined = undefined;
57
+ export let stream_handler: Client["stream"] | undefined = undefined;
46
58
 
47
59
  let is_full_screen = false;
48
60
  let image_container: HTMLElement;
@@ -53,6 +65,9 @@
53
65
  preview_open: undefined;
54
66
  preview_close: undefined;
55
67
  fullscreen: boolean;
68
+ delete: { file: FileData; index: number };
69
+ upload: FileData | FileData[];
70
+ error: string;
56
71
  }>();
57
72
 
58
73
  // tracks whether the value of the gallery was reset
@@ -77,6 +92,21 @@
77
92
  return {};
78
93
  }) as GalleryData[]);
79
94
 
95
+ let effective_columns: number | number[] | undefined = columns;
96
+
97
+ $: {
98
+ if (resolved_value && columns) {
99
+ const item_count = resolved_value.length;
100
+ if (Array.isArray(columns)) {
101
+ effective_columns = columns.map((col) => Math.min(col, item_count));
102
+ } else {
103
+ effective_columns = Math.min(columns, item_count);
104
+ }
105
+ } else {
106
+ effective_columns = columns;
107
+ }
108
+ }
109
+
80
110
  let prev_value: GalleryData[] | null = value;
81
111
  if (selected_index == null && preview && value?.length) {
82
112
  selected_index = 0;
@@ -253,6 +283,31 @@
253
283
  $: if (container_element) {
254
284
  check_thumbnails_overflow();
255
285
  }
286
+
287
+ function handle_item_delete(index: number): void {
288
+ if (!value || !resolved_value) return;
289
+
290
+ const deleted_item = resolved_value[index];
291
+ let deleted_file_data;
292
+
293
+ if ("image" in deleted_item) {
294
+ deleted_file_data = {
295
+ file: deleted_item.image,
296
+ index: index
297
+ };
298
+ } else if ("video" in deleted_item) {
299
+ deleted_file_data = {
300
+ file: deleted_item.video,
301
+ index: index
302
+ };
303
+ }
304
+
305
+ if (deleted_file_data) {
306
+ dispatch("delete", deleted_file_data);
307
+ }
308
+ }
309
+
310
+ let uploading = false;
256
311
  </script>
257
312
 
258
313
  <svelte:window bind:innerHeight={window_height} />
@@ -408,51 +463,82 @@
408
463
  class:hidden={is_full_screen}
409
464
  >
410
465
  {#if interactive && selected_index === null}
411
- <ModifyUpload {i18n} on:clear={() => (value = [])} />
466
+ <ModifyUpload {i18n} on:clear={() => (value = [])}>
467
+ {#if upload && stream_handler}
468
+ <IconButton Icon={UploadIcon} label={i18n("common.upload")}>
469
+ <UploadComponent
470
+ icon_upload={true}
471
+ on:load={(e) => dispatch("upload", e.detail)}
472
+ filetype={file_types}
473
+ file_count="multiple"
474
+ {max_file_size}
475
+ {root}
476
+ bind:uploading
477
+ on:error={(e) => dispatch("error", e.detail)}
478
+ {stream_handler}
479
+ {upload}
480
+ />
481
+ </IconButton>
482
+ {/if}
483
+ </ModifyUpload>
412
484
  {/if}
413
485
  <div
414
486
  class="grid-container"
415
- style="--grid-cols:{columns}; --grid-rows:{rows}; --object-fit: {object_fit}; height: {height};"
487
+ style="--grid-cols:{effective_columns}; --grid-rows:{rows}; --object-fit: {object_fit}; height: {height};"
416
488
  class:pt-6={show_label}
417
489
  >
418
490
  {#each resolved_value as entry, i}
419
- <button
420
- class="thumbnail-item thumbnail-lg"
421
- class:selected={selected_index === i}
422
- on:click={() => {
423
- if (selected_index === null && allow_preview) {
424
- dispatch("preview_open");
425
- }
426
- selected_index = i;
427
- }}
428
- aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
429
- >
430
- {#if "image" in entry}
431
- <Image
432
- alt={entry.caption || ""}
433
- src={typeof entry.image === "string"
434
- ? entry.image
435
- : entry.image.url}
436
- loading="lazy"
437
- />
438
- {:else}
439
- <Play />
440
- <Video
441
- src={entry.video.url}
442
- title={entry.caption || null}
443
- is_stream={false}
444
- data-testid={"thumbnail " + (i + 1)}
445
- alt=""
446
- loading="lazy"
447
- loop={false}
448
- />
449
- {/if}
450
- {#if entry.caption}
451
- <div class="caption-label">
452
- {entry.caption}
453
- </div>
491
+ <div class="gallery-item">
492
+ <button
493
+ class="thumbnail-item thumbnail-lg"
494
+ class:selected={selected_index === i}
495
+ on:click={() => {
496
+ if (selected_index === null && allow_preview) {
497
+ dispatch("preview_open");
498
+ }
499
+ selected_index = i;
500
+ }}
501
+ aria-label={"Thumbnail " +
502
+ (i + 1) +
503
+ " of " +
504
+ resolved_value.length}
505
+ >
506
+ {#if "image" in entry}
507
+ <Image
508
+ alt={entry.caption || ""}
509
+ src={typeof entry.image === "string"
510
+ ? entry.image
511
+ : entry.image.url}
512
+ loading="lazy"
513
+ />
514
+ {:else}
515
+ <Play />
516
+ <Video
517
+ src={entry.video.url}
518
+ title={entry.caption || null}
519
+ is_stream={false}
520
+ data-testid={"thumbnail " + (i + 1)}
521
+ alt=""
522
+ loading="lazy"
523
+ loop={false}
524
+ />
525
+ {/if}
526
+ {#if entry.caption}
527
+ <div class="caption-label">
528
+ {entry.caption}
529
+ </div>
530
+ {/if}
531
+ </button>
532
+ {#if interactive}
533
+ <button
534
+ class="delete-button"
535
+ on:click|stopPropagation={() => handle_item_delete(i)}
536
+ aria-label="Delete image"
537
+ >
538
+ <Clear />
539
+ </button>
454
540
  {/if}
455
- </button>
541
+ </div>
456
542
  {/each}
457
543
  </div>
458
544
  </div>
@@ -681,4 +767,49 @@
681
767
  .grid-wrap.minimal {
682
768
  padding: 0;
683
769
  }
770
+
771
+ .gallery-item {
772
+ position: relative;
773
+ width: 100%;
774
+ height: 100%;
775
+ }
776
+
777
+ .delete-button {
778
+ position: absolute;
779
+ bottom: 0;
780
+ left: 0;
781
+ z-index: var(--layer-1);
782
+ border-top: 1px solid var(--border-color-primary);
783
+ border-right: 1px solid var(--border-color-primary);
784
+ border-radius: 0 var(--radius-sm) 0 var(--radius-sm);
785
+ background: var(--background-fill-secondary);
786
+ padding: var(--block-label-padding);
787
+ cursor: pointer;
788
+ display: flex;
789
+ align-items: center;
790
+ justify-content: center;
791
+ opacity: 0;
792
+ transition: opacity 0.2s ease;
793
+ font-size: var(--block-label-text-size);
794
+ color: var(--block-label-text-color);
795
+ font-weight: var(--weight-semibold);
796
+ width: auto;
797
+ height: auto;
798
+ min-width: fit-content;
799
+ min-height: fit-content;
800
+ }
801
+
802
+ .gallery-item:hover .delete-button {
803
+ opacity: 1;
804
+ }
805
+
806
+ .delete-button:hover {
807
+ opacity: 0.8;
808
+ }
809
+
810
+ .delete-button :global(svg) {
811
+ width: var(--text-xs);
812
+ height: var(--text-md);
813
+ color: var(--block-label-text-color);
814
+ }
684
815
  </style>