elismasilva commited on
Commit
e07925b
·
verified ·
1 Parent(s): 93395e9

Upload folder using huggingface_hub

Browse files
Files changed (47) hide show
  1. .gitattributes +8 -0
  2. .gitignore +13 -0
  3. README.md +567 -12
  4. __init__.py +0 -0
  5. app.py +108 -0
  6. css.css +157 -0
  7. requirements.txt +2 -0
  8. space.py +229 -0
  9. src/.gitignore +13 -0
  10. src/.vscode/launch.json +29 -0
  11. src/README.md +567 -0
  12. src/README_TEMPLATE.md +565 -0
  13. src/backend/gradio_mediagallery/__init__.py +4 -0
  14. src/backend/gradio_mediagallery/helpers.py +315 -0
  15. src/backend/gradio_mediagallery/mediagallery.py +281 -0
  16. src/backend/gradio_mediagallery/templates/component/assets/worker-BAOIWoxA.js +1 -0
  17. src/backend/gradio_mediagallery/templates/component/index.js +0 -0
  18. src/backend/gradio_mediagallery/templates/component/style.css +1 -0
  19. src/backend/gradio_mediagallery/templates/example/index.js +342 -0
  20. src/backend/gradio_mediagallery/templates/example/style.css +1 -0
  21. src/demo/__init__.py +0 -0
  22. src/demo/app.py +108 -0
  23. src/demo/css.css +157 -0
  24. src/demo/requirements.txt +2 -0
  25. src/demo/space.py +229 -0
  26. src/examples/folder1/Jensen.jpeg +3 -0
  27. src/examples/folder1/newton_0.jpg +3 -0
  28. src/examples/folder1/newton_2.png +3 -0
  29. src/examples/folder1/newton_3.jpg +0 -0
  30. src/examples/folder1/sam1.jpg +3 -0
  31. src/examples/folder2/SampleVideo 720x480.mp4 +3 -0
  32. src/examples/folder2/butterfly_input.jpg +3 -0
  33. src/examples/folder2/lemons_input.jpg +3 -0
  34. src/examples/folder2/vermeer.jpg +0 -0
  35. src/examples/image_with_meta.png +3 -0
  36. src/frontend/Example.svelte +143 -0
  37. src/frontend/Gallery.css +471 -0
  38. src/frontend/Index.svelte +105 -0
  39. src/frontend/gradio.config.js +9 -0
  40. src/frontend/package-lock.json +0 -0
  41. src/frontend/package.json +54 -0
  42. src/frontend/shared/Gallery.svelte +1099 -0
  43. src/frontend/shared/utils.ts +18 -0
  44. src/frontend/tsconfig.json +14 -0
  45. src/frontend/types.ts +11 -0
  46. src/log.txt +0 -0
  47. src/pyproject.toml +54 -0
.gitattributes CHANGED
@@ -33,3 +33,11 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ src/examples/folder1/Jensen.jpeg filter=lfs diff=lfs merge=lfs -text
37
+ src/examples/folder1/newton_0.jpg filter=lfs diff=lfs merge=lfs -text
38
+ src/examples/folder1/newton_2.png filter=lfs diff=lfs merge=lfs -text
39
+ src/examples/folder1/sam1.jpg filter=lfs diff=lfs merge=lfs -text
40
+ src/examples/folder2/butterfly_input.jpg filter=lfs diff=lfs merge=lfs -text
41
+ src/examples/folder2/lemons_input.jpg filter=lfs diff=lfs merge=lfs -text
42
+ src/examples/folder2/SampleVideo[[:space:]]720x480.mp4 filter=lfs diff=lfs merge=lfs -text
43
+ src/examples/image_with_meta.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .eggs/
2
+ dist/
3
+ .vscode/
4
+ *.pyc
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ __tmp/*
9
+ *.pyi
10
+ .mypycache
11
+ .ruff_cache
12
+ node_modules
13
+ backend/**/templates/
README.md CHANGED
@@ -1,12 +1,567 @@
1
- ---
2
- title: Gradio Mediagallery
3
- emoji: 🔥
4
- colorFrom: blue
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 5.49.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags: [gradio-custom-component, gallery]
3
+ title: gradio_mediagallery
4
+ short_description: A gradio custom component
5
+ colorFrom: blue
6
+ colorTo: yellow
7
+ sdk: gradio
8
+ pinned: false
9
+ app_file: space.py
10
+ ---
11
+
12
+ # `gradio_mediagallery`
13
+ <img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20blue"> <a href="https://huggingface.co/spaces/elismasilva/gradio_mediagallery"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Demo-blue"></a><p><span>💻 <a href='https://github.com/DEVAIEXP/gradio_component_mediagallery'>Component GitHub Code</a></span></p>
14
+
15
+ Python library for easily interacting with trained machine learning models
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install gradio_mediagallery gradio_folderexplorer
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from typing import Any, List
27
+ import gradio as gr
28
+ from gradio_folderexplorer import FolderExplorer
29
+ from gradio_folderexplorer.helpers import load_media_from_folder
30
+ from gradio_mediagallery import MediaGallery
31
+ from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
32
+ import os
33
+
34
+ # Configuration constant for the root directory containing media files
35
+ ROOT_DIR_PATH = "./src/examples"
36
+
37
+ def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
38
+ """
39
+ Processes image metadata by calling the `transfer_metadata` helper.
40
+
41
+ Args:
42
+ image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
43
+
44
+ Returns:
45
+ List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
46
+ """
47
+ if not image_data or not hasattr(image_data, "_data"):
48
+ return [gr.skip()] * len(output_fields)
49
+
50
+ return transfer_metadata(
51
+ output_fields=output_fields,
52
+ metadata=image_data._data,
53
+ remove_prefix_from_keys=True
54
+ )
55
+
56
+ # UI layout and logic
57
+ with gr.Blocks() as demo:
58
+ """
59
+ A Gradio interface for browsing and displaying media files with metadata extraction.
60
+ """
61
+ gr.Markdown("# MediaGallery with Metadata Extraction")
62
+ gr.Markdown(
63
+ """
64
+ **To Test:**
65
+ 1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
66
+ 2. Click on an image in the **Media Gallery** to open the preview mode.
67
+ 3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
68
+ 4. Click the **'Load Metadata'** button inside the popup.
69
+ 5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
70
+ """
71
+ )
72
+ with gr.Row(equal_height=True):
73
+ with gr.Column(scale=1, min_width=300):
74
+ folder_explorer = FolderExplorer(
75
+ label="Select a Folder",
76
+ root_dir=ROOT_DIR_PATH,
77
+ value=ROOT_DIR_PATH
78
+ )
79
+
80
+ with gr.Column(scale=3):
81
+ gallery = MediaGallery(
82
+ label="Media in Folder",
83
+ columns=6,
84
+ height="auto",
85
+ preview=False,
86
+ show_download_button=False,
87
+ only_custom_metadata=False,
88
+ popup_metadata_width="40%",
89
+ )
90
+
91
+ gr.Markdown("## Metadata Viewer")
92
+ with gr.Row():
93
+ model_box = gr.Textbox(label="Model")
94
+ fnumber_box = gr.Textbox(label="FNumber")
95
+ iso_box = gr.Textbox(label="ISOSpeedRatings")
96
+ s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
97
+ description_box = gr.Textbox(label="Description", lines=2)
98
+
99
+ # Event handling
100
+ output_fields = [
101
+ model_box,
102
+ fnumber_box,
103
+ iso_box,
104
+ s_churn,
105
+ description_box
106
+ ]
107
+
108
+ # Populate the gallery when the folder changes
109
+ folder_explorer.change(
110
+ fn=load_media_from_folder,
111
+ inputs=folder_explorer,
112
+ outputs=gallery
113
+ )
114
+
115
+ # Populate the gallery on initial load
116
+ demo.load(
117
+ fn=load_media_from_folder,
118
+ inputs=folder_explorer,
119
+ outputs=gallery
120
+ )
121
+
122
+ # Handle the load_metadata event from MediaGallery
123
+ gallery.load_metadata(
124
+ fn=handle_load_metadata,
125
+ inputs=None,
126
+ outputs=output_fields
127
+ )
128
+
129
+ if __name__ == "__main__":
130
+ """
131
+ Launches the Gradio interface in debug mode.
132
+ """
133
+ demo.launch(debug=True)
134
+ ```
135
+
136
+ ## `MediaGallery`
137
+
138
+ ### Initialization
139
+
140
+ <table>
141
+ <thead>
142
+ <tr>
143
+ <th align="left">name</th>
144
+ <th align="left" style="width: 25%;">type</th>
145
+ <th align="left">default</th>
146
+ <th align="left">description</th>
147
+ </tr>
148
+ </thead>
149
+ <tbody>
150
+ <tr>
151
+ <td align="left"><code>value</code></td>
152
+ <td align="left" style="width: 25%;">
153
+
154
+ ```python
155
+ Sequence[
156
+ np.ndarray | PIL.Image.Image | str | Path | tuple
157
+ ]
158
+ | Callable
159
+ | None
160
+ ```
161
+
162
+ </td>
163
+ <td align="left"><code>None</code></td>
164
+ <td align="left">List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.</td>
165
+ </tr>
166
+
167
+ <tr>
168
+ <td align="left"><code>file_types</code></td>
169
+ <td align="left" style="width: 25%;">
170
+
171
+ ```python
172
+ list[str] | None
173
+ ```
174
+
175
+ </td>
176
+ <td align="left"><code>None</code></td>
177
+ <td align="left">List of file extensions or types of files to be uploaded (e.g. ['image', '.mp4']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.</td>
178
+ </tr>
179
+
180
+ <tr>
181
+ <td align="left"><code>label</code></td>
182
+ <td align="left" style="width: 25%;">
183
+
184
+ ```python
185
+ str | I18nData | None
186
+ ```
187
+
188
+ </td>
189
+ <td align="left"><code>None</code></td>
190
+ <td align="left">the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td>
191
+ </tr>
192
+
193
+ <tr>
194
+ <td align="left"><code>every</code></td>
195
+ <td align="left" style="width: 25%;">
196
+
197
+ ```python
198
+ Timer | float | None
199
+ ```
200
+
201
+ </td>
202
+ <td align="left"><code>None</code></td>
203
+ <td align="left">Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.</td>
204
+ </tr>
205
+
206
+ <tr>
207
+ <td align="left"><code>inputs</code></td>
208
+ <td align="left" style="width: 25%;">
209
+
210
+ ```python
211
+ Component | Sequence[Component] | set[Component] | None
212
+ ```
213
+
214
+ </td>
215
+ <td align="left"><code>None</code></td>
216
+ <td align="left">Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.</td>
217
+ </tr>
218
+
219
+ <tr>
220
+ <td align="left"><code>show_label</code></td>
221
+ <td align="left" style="width: 25%;">
222
+
223
+ ```python
224
+ bool | None
225
+ ```
226
+
227
+ </td>
228
+ <td align="left"><code>None</code></td>
229
+ <td align="left">if True, will display label.</td>
230
+ </tr>
231
+
232
+ <tr>
233
+ <td align="left"><code>container</code></td>
234
+ <td align="left" style="width: 25%;">
235
+
236
+ ```python
237
+ bool
238
+ ```
239
+
240
+ </td>
241
+ <td align="left"><code>True</code></td>
242
+ <td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
243
+ </tr>
244
+
245
+ <tr>
246
+ <td align="left"><code>scale</code></td>
247
+ <td align="left" style="width: 25%;">
248
+
249
+ ```python
250
+ int | None
251
+ ```
252
+
253
+ </td>
254
+ <td align="left"><code>None</code></td>
255
+ <td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
256
+ </tr>
257
+
258
+ <tr>
259
+ <td align="left"><code>min_width</code></td>
260
+ <td align="left" style="width: 25%;">
261
+
262
+ ```python
263
+ int
264
+ ```
265
+
266
+ </td>
267
+ <td align="left"><code>160</code></td>
268
+ <td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
269
+ </tr>
270
+
271
+ <tr>
272
+ <td align="left"><code>visible</code></td>
273
+ <td align="left" style="width: 25%;">
274
+
275
+ ```python
276
+ bool | Literal["hidden"]
277
+ ```
278
+
279
+ </td>
280
+ <td align="left"><code>True</code></td>
281
+ <td align="left">If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM</td>
282
+ </tr>
283
+
284
+ <tr>
285
+ <td align="left"><code>elem_id</code></td>
286
+ <td align="left" style="width: 25%;">
287
+
288
+ ```python
289
+ str | None
290
+ ```
291
+
292
+ </td>
293
+ <td align="left"><code>None</code></td>
294
+ <td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
295
+ </tr>
296
+
297
+ <tr>
298
+ <td align="left"><code>elem_classes</code></td>
299
+ <td align="left" style="width: 25%;">
300
+
301
+ ```python
302
+ list[str] | str | None
303
+ ```
304
+
305
+ </td>
306
+ <td align="left"><code>None</code></td>
307
+ <td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
308
+ </tr>
309
+
310
+ <tr>
311
+ <td align="left"><code>render</code></td>
312
+ <td align="left" style="width: 25%;">
313
+
314
+ ```python
315
+ bool
316
+ ```
317
+
318
+ </td>
319
+ <td align="left"><code>True</code></td>
320
+ <td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
321
+ </tr>
322
+
323
+ <tr>
324
+ <td align="left"><code>key</code></td>
325
+ <td align="left" style="width: 25%;">
326
+
327
+ ```python
328
+ int | str | tuple[int | str, ...] | None
329
+ ```
330
+
331
+ </td>
332
+ <td align="left"><code>None</code></td>
333
+ <td align="left">in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.</td>
334
+ </tr>
335
+
336
+ <tr>
337
+ <td align="left"><code>preserved_by_key</code></td>
338
+ <td align="left" style="width: 25%;">
339
+
340
+ ```python
341
+ list[str] | str | None
342
+ ```
343
+
344
+ </td>
345
+ <td align="left"><code>"value"</code></td>
346
+ <td align="left">A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.</td>
347
+ </tr>
348
+
349
+ <tr>
350
+ <td align="left"><code>columns</code></td>
351
+ <td align="left" style="width: 25%;">
352
+
353
+ ```python
354
+ int | None
355
+ ```
356
+
357
+ </td>
358
+ <td align="left"><code>2</code></td>
359
+ <td align="left">Represents the number of images that should be shown in one row.</td>
360
+ </tr>
361
+
362
+ <tr>
363
+ <td align="left"><code>rows</code></td>
364
+ <td align="left" style="width: 25%;">
365
+
366
+ ```python
367
+ int | None
368
+ ```
369
+
370
+ </td>
371
+ <td align="left"><code>None</code></td>
372
+ <td align="left">Represents the number of rows in the image grid.</td>
373
+ </tr>
374
+
375
+ <tr>
376
+ <td align="left"><code>height</code></td>
377
+ <td align="left" style="width: 25%;">
378
+
379
+ ```python
380
+ int | float | str | None
381
+ ```
382
+
383
+ </td>
384
+ <td align="left"><code>None</code></td>
385
+ <td align="left">The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.</td>
386
+ </tr>
387
+
388
+ <tr>
389
+ <td align="left"><code>allow_preview</code></td>
390
+ <td align="left" style="width: 25%;">
391
+
392
+ ```python
393
+ bool
394
+ ```
395
+
396
+ </td>
397
+ <td align="left"><code>True</code></td>
398
+ <td align="left">If True, images in the gallery will be enlarged when they are clicked. Default is True.</td>
399
+ </tr>
400
+
401
+ <tr>
402
+ <td align="left"><code>preview</code></td>
403
+ <td align="left" style="width: 25%;">
404
+
405
+ ```python
406
+ bool | None
407
+ ```
408
+
409
+ </td>
410
+ <td align="left"><code>None</code></td>
411
+ <td align="left">If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.</td>
412
+ </tr>
413
+
414
+ <tr>
415
+ <td align="left"><code>selected_index</code></td>
416
+ <td align="left" style="width: 25%;">
417
+
418
+ ```python
419
+ int | None
420
+ ```
421
+
422
+ </td>
423
+ <td align="left"><code>None</code></td>
424
+ <td align="left">The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.</td>
425
+ </tr>
426
+
427
+ <tr>
428
+ <td align="left"><code>object_fit</code></td>
429
+ <td align="left" style="width: 25%;">
430
+
431
+ ```python
432
+ Literal[
433
+ "contain", "cover", "fill", "none", "scale-down"
434
+ ]
435
+ | None
436
+ ```
437
+
438
+ </td>
439
+ <td align="left"><code>None</code></td>
440
+ <td align="left">CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".</td>
441
+ </tr>
442
+
443
+ <tr>
444
+ <td align="left"><code>show_share_button</code></td>
445
+ <td align="left" style="width: 25%;">
446
+
447
+ ```python
448
+ bool | None
449
+ ```
450
+
451
+ </td>
452
+ <td align="left"><code>None</code></td>
453
+ <td align="left">If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
454
+ </tr>
455
+
456
+ <tr>
457
+ <td align="left"><code>show_download_button</code></td>
458
+ <td align="left" style="width: 25%;">
459
+
460
+ ```python
461
+ bool | None
462
+ ```
463
+
464
+ </td>
465
+ <td align="left"><code>True</code></td>
466
+ <td align="left">If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.</td>
467
+ </tr>
468
+
469
+ <tr>
470
+ <td align="left"><code>interactive</code></td>
471
+ <td align="left" style="width: 25%;">
472
+
473
+ ```python
474
+ bool | None
475
+ ```
476
+
477
+ </td>
478
+ <td align="left"><code>None</code></td>
479
+ <td align="left">If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.</td>
480
+ </tr>
481
+
482
+ <tr>
483
+ <td align="left"><code>type</code></td>
484
+ <td align="left" style="width: 25%;">
485
+
486
+ ```python
487
+ Literal["numpy", "pil", "filepath"]
488
+ ```
489
+
490
+ </td>
491
+ <td align="left"><code>"filepath"</code></td>
492
+ <td align="left">The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.</td>
493
+ </tr>
494
+
495
+ <tr>
496
+ <td align="left"><code>show_fullscreen_button</code></td>
497
+ <td align="left" style="width: 25%;">
498
+
499
+ ```python
500
+ bool
501
+ ```
502
+
503
+ </td>
504
+ <td align="left"><code>True</code></td>
505
+ <td align="left">If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
506
+ </tr>
507
+
508
+ <tr>
509
+ <td align="left"><code>only_custom_metadata</code></td>
510
+ <td align="left" style="width: 25%;">
511
+
512
+ ```python
513
+ bool
514
+ ```
515
+
516
+ </td>
517
+ <td align="left"><code>True</code></td>
518
+ <td align="left">If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.</td>
519
+ </tr>
520
+
521
+ <tr>
522
+ <td align="left"><code>popup_metadata_width</code></td>
523
+ <td align="left" style="width: 25%;">
524
+
525
+ ```python
526
+ int | str
527
+ ```
528
+
529
+ </td>
530
+ <td align="left"><code>500</code></td>
531
+ <td align="left">The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").</td>
532
+ </tr>
533
+ </tbody></table>
534
+
535
+
536
+ ### Events
537
+
538
+ | name | description |
539
+ |:-----|:------------|
540
+ | `select` | Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data |
541
+ | `change` | Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. |
542
+ | `delete` | This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data |
543
+ | `preview_close` | This event is triggered when the MediaGallery preview is closed by the user |
544
+ | `preview_open` | This event is triggered when the MediaGallery preview is opened by the user |
545
+ | `load_metadata` | Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata. |
546
+
547
+
548
+
549
+ ### User function
550
+
551
+ The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
552
+
553
+ - When used as an Input, the component only impacts the input signature of the user function.
554
+ - When used as an output, the component only impacts the return signature of the user function.
555
+
556
+ The code snippet below is accurate in cases where the component is used as both an input and an output.
557
+
558
+ - **As output:** Is passed, the preprocessed input data sent to the user's function in the backend.
559
+ - **As input:** Should return, the output data received by the component from the user's function in the backend.
560
+
561
+ ```python
562
+ def predict(
563
+ value: Any
564
+ ) -> list | None:
565
+ return value
566
+ ```
567
+
__init__.py ADDED
File without changes
app.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, List
2
+ import gradio as gr
3
+ from gradio_folderexplorer import FolderExplorer
4
+ from gradio_folderexplorer.helpers import load_media_from_folder
5
+ from gradio_mediagallery import MediaGallery
6
+ from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
7
+ import os
8
+
9
+ # Configuration constant for the root directory containing media files
10
+ ROOT_DIR_PATH = "./src/examples"
11
+
12
+ def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
13
+ """
14
+ Processes image metadata by calling the `transfer_metadata` helper.
15
+
16
+ Args:
17
+ image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
18
+
19
+ Returns:
20
+ List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
21
+ """
22
+ if not image_data or not hasattr(image_data, "_data"):
23
+ return [gr.skip()] * len(output_fields)
24
+
25
+ return transfer_metadata(
26
+ output_fields=output_fields,
27
+ metadata=image_data._data,
28
+ remove_prefix_from_keys=True
29
+ )
30
+
31
+ # UI layout and logic
32
+ with gr.Blocks() as demo:
33
+ """
34
+ A Gradio interface for browsing and displaying media files with metadata extraction.
35
+ """
36
+ gr.Markdown("# MediaGallery with Metadata Extraction")
37
+ gr.Markdown(
38
+ """
39
+ **To Test:**
40
+ 1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
41
+ 2. Click on an image in the **Media Gallery** to open the preview mode.
42
+ 3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
43
+ 4. Click the **'Load Metadata'** button inside the popup.
44
+ 5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
45
+ """
46
+ )
47
+ with gr.Row(equal_height=True):
48
+ with gr.Column(scale=1, min_width=300):
49
+ folder_explorer = FolderExplorer(
50
+ label="Select a Folder",
51
+ root_dir=ROOT_DIR_PATH,
52
+ value=ROOT_DIR_PATH
53
+ )
54
+
55
+ with gr.Column(scale=3):
56
+ gallery = MediaGallery(
57
+ label="Media in Folder",
58
+ columns=6,
59
+ height="auto",
60
+ preview=False,
61
+ show_download_button=False,
62
+ only_custom_metadata=False,
63
+ popup_metadata_width="40%",
64
+ )
65
+
66
+ gr.Markdown("## Metadata Viewer")
67
+ with gr.Row():
68
+ model_box = gr.Textbox(label="Model")
69
+ fnumber_box = gr.Textbox(label="FNumber")
70
+ iso_box = gr.Textbox(label="ISOSpeedRatings")
71
+ s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
72
+ description_box = gr.Textbox(label="Description", lines=2)
73
+
74
+ # Event handling
75
+ output_fields = [
76
+ model_box,
77
+ fnumber_box,
78
+ iso_box,
79
+ s_churn,
80
+ description_box
81
+ ]
82
+
83
+ # Populate the gallery when the folder changes
84
+ folder_explorer.change(
85
+ fn=load_media_from_folder,
86
+ inputs=folder_explorer,
87
+ outputs=gallery
88
+ )
89
+
90
+ # Populate the gallery on initial load
91
+ demo.load(
92
+ fn=load_media_from_folder,
93
+ inputs=folder_explorer,
94
+ outputs=gallery
95
+ )
96
+
97
+ # Handle the load_metadata event from MediaGallery
98
+ gallery.load_metadata(
99
+ fn=handle_load_metadata,
100
+ inputs=None,
101
+ outputs=output_fields
102
+ )
103
+
104
+ if __name__ == "__main__":
105
+ """
106
+ Launches the Gradio interface in debug mode.
107
+ """
108
+ demo.launch(debug=True)
css.css ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html {
2
+ font-family: Inter;
3
+ font-size: 16px;
4
+ font-weight: 400;
5
+ line-height: 1.5;
6
+ -webkit-text-size-adjust: 100%;
7
+ background: #fff;
8
+ color: #323232;
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ text-rendering: optimizeLegibility;
12
+ }
13
+
14
+ :root {
15
+ --space: 1;
16
+ --vspace: calc(var(--space) * 1rem);
17
+ --vspace-0: calc(3 * var(--space) * 1rem);
18
+ --vspace-1: calc(2 * var(--space) * 1rem);
19
+ --vspace-2: calc(1.5 * var(--space) * 1rem);
20
+ --vspace-3: calc(0.5 * var(--space) * 1rem);
21
+ }
22
+
23
+ .app {
24
+ max-width: 748px !important;
25
+ }
26
+
27
+ .prose p {
28
+ margin: var(--vspace) 0;
29
+ line-height: var(--vspace * 2);
30
+ font-size: 1rem;
31
+ }
32
+
33
+ code {
34
+ font-family: "Inconsolata", sans-serif;
35
+ font-size: 16px;
36
+ }
37
+
38
+ h1,
39
+ h1 code {
40
+ font-weight: 400;
41
+ line-height: calc(2.5 / var(--space) * var(--vspace));
42
+ }
43
+
44
+ h1 code {
45
+ background: none;
46
+ border: none;
47
+ letter-spacing: 0.05em;
48
+ padding-bottom: 5px;
49
+ position: relative;
50
+ padding: 0;
51
+ }
52
+
53
+ h2 {
54
+ margin: var(--vspace-1) 0 var(--vspace-2) 0;
55
+ line-height: 1em;
56
+ }
57
+
58
+ h3,
59
+ h3 code {
60
+ margin: var(--vspace-1) 0 var(--vspace-2) 0;
61
+ line-height: 1em;
62
+ }
63
+
64
+ h4,
65
+ h5,
66
+ h6 {
67
+ margin: var(--vspace-3) 0 var(--vspace-3) 0;
68
+ line-height: var(--vspace);
69
+ }
70
+
71
+ .bigtitle,
72
+ h1,
73
+ h1 code {
74
+ font-size: calc(8px * 4.5);
75
+ word-break: break-word;
76
+ }
77
+
78
+ .title,
79
+ h2,
80
+ h2 code {
81
+ font-size: calc(8px * 3.375);
82
+ font-weight: lighter;
83
+ word-break: break-word;
84
+ border: none;
85
+ background: none;
86
+ }
87
+
88
+ .subheading1,
89
+ h3,
90
+ h3 code {
91
+ font-size: calc(8px * 1.8);
92
+ font-weight: 600;
93
+ border: none;
94
+ background: none;
95
+ letter-spacing: 0.1em;
96
+ text-transform: uppercase;
97
+ }
98
+
99
+ h2 code {
100
+ padding: 0;
101
+ position: relative;
102
+ letter-spacing: 0.05em;
103
+ }
104
+
105
+ blockquote {
106
+ font-size: calc(8px * 1.1667);
107
+ font-style: italic;
108
+ line-height: calc(1.1667 * var(--vspace));
109
+ margin: var(--vspace-2) var(--vspace-2);
110
+ }
111
+
112
+ .subheading2,
113
+ h4 {
114
+ font-size: calc(8px * 1.4292);
115
+ text-transform: uppercase;
116
+ font-weight: 600;
117
+ }
118
+
119
+ .subheading3,
120
+ h5 {
121
+ font-size: calc(8px * 1.2917);
122
+ line-height: calc(1.2917 * var(--vspace));
123
+
124
+ font-weight: lighter;
125
+ text-transform: uppercase;
126
+ letter-spacing: 0.15em;
127
+ }
128
+
129
+ h6 {
130
+ font-size: calc(8px * 1.1667);
131
+ font-size: 1.1667em;
132
+ font-weight: normal;
133
+ font-style: italic;
134
+ font-family: "le-monde-livre-classic-byol", serif !important;
135
+ letter-spacing: 0px !important;
136
+ }
137
+
138
+ #start .md > *:first-child {
139
+ margin-top: 0;
140
+ }
141
+
142
+ h2 + h3 {
143
+ margin-top: 0;
144
+ }
145
+
146
+ .md hr {
147
+ border: none;
148
+ border-top: 1px solid var(--block-border-color);
149
+ margin: var(--vspace-2) 0 var(--vspace-2) 0;
150
+ }
151
+ .prose ul {
152
+ margin: var(--vspace-2) 0 var(--vspace-1) 0;
153
+ }
154
+
155
+ .gap {
156
+ gap: 0;
157
+ }
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio_mediagallery
2
+ gradio_folderexplorer
space.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ from app import demo as app
4
+ import os
5
+
6
+ _docs = {'MediaGallery': {'description': 'Creates a gallery component that allows displaying a grid of images or videos, and optionally captions. If used as an input, the user can upload images or videos to the gallery.\nIf used as an output, the user can click on individual images or videos to view them at a higher resolution.\n', 'members': {'__init__': {'value': {'type': 'Sequence[\n np.ndarray | PIL.Image.Image | str | Path | tuple\n ]\n | Callable\n | None', 'default': 'None', 'description': 'List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.'}, 'file_types': {'type': 'list[str] | None', 'default': 'None', 'description': 'List of file extensions or types of files to be uploaded (e.g. [\'image\', \'.mp4\']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.'}, 'label': {'type': 'str | I18nData | None', 'default': 'None', 'description': 'the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'every': {'type': 'Timer | float | None', 'default': 'None', 'description': 'Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.'}, 'inputs': {'type': 'Component | Sequence[Component] | set[Component] | None', 'default': 'None', 'description': 'Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'visible': {'type': 'bool | Literal["hidden"]', 'default': 'True', 'description': 'If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'key': {'type': 'int | str | tuple[int | str, ...] | None', 'default': 'None', 'description': "in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render."}, 'preserved_by_key': {'type': 'list[str] | str | None', 'default': '"value"', 'description': "A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor."}, 'columns': {'type': 'int | None', 'default': '2', 'description': 'Represents the number of images that should be shown in one row.'}, 'rows': {'type': 'int | None', 'default': 'None', 'description': 'Represents the number of rows in the image grid.'}, 'height': {'type': 'int | float | str | None', 'default': 'None', 'description': 'The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.'}, 'allow_preview': {'type': 'bool', 'default': 'True', 'description': 'If True, images in the gallery will be enlarged when they are clicked. Default is True.'}, 'preview': {'type': 'bool | None', 'default': 'None', 'description': 'If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.'}, 'selected_index': {'type': 'int | None', 'default': 'None', 'description': 'The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.'}, 'object_fit': {'type': 'Literal[\n "contain", "cover", "fill", "none", "scale-down"\n ]\n | None', 'default': 'None', 'description': 'CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".'}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'show_download_button': {'type': 'bool | None', 'default': 'True', 'description': 'If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.'}, 'interactive': {'type': 'bool | None', 'default': 'None', 'description': 'If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.'}, 'type': {'type': 'Literal["numpy", "pil", "filepath"]', 'default': '"filepath"', 'description': 'The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.'}, 'show_fullscreen_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'only_custom_metadata': {'type': 'bool', 'default': 'True', 'description': 'If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.'}, 'popup_metadata_width': {'type': 'int | str', 'default': '500', 'description': 'The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").'}}, 'postprocess': {'value': {'type': 'list | None', 'description': "The output data received by the component from the user's function in the backend."}}, 'preprocess': {'return': {'type': 'Any', 'description': "The preprocessed input data sent to the user's function in the backend."}, 'value': None}}, 'events': {'select': {'type': None, 'default': None, 'description': 'Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'delete': {'type': None, 'default': None, 'description': 'This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data'}, 'preview_close': {'type': None, 'default': None, 'description': 'This event is triggered when the MediaGallery preview is closed by the user'}, 'preview_open': {'type': None, 'default': None, 'description': 'This event is triggered when the MediaGallery preview is opened by the user'}, 'load_metadata': {'type': None, 'default': None, 'description': "Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata."}}}, '__meta__': {'additional_interfaces': {}, 'user_fn_refs': {'MediaGallery': []}}}
7
+
8
+ abs_path = os.path.join(os.path.dirname(__file__), "css.css")
9
+
10
+ with gr.Blocks(
11
+ css=abs_path,
12
+ theme=gr.themes.Ocean(
13
+ font_mono=[
14
+ gr.themes.GoogleFont("Inconsolata"),
15
+ "monospace",
16
+ ],
17
+ ),
18
+ ) as demo:
19
+ gr.Markdown(
20
+ """
21
+ # `gradio_mediagallery`
22
+
23
+ <div style="display: flex; gap: 7px;">
24
+ <a href="https://pypi.org/project/gradio_mediagallery/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_mediagallery"></a>
25
+ </div>
26
+
27
+ Python library for easily interacting with trained machine learning models
28
+ """, elem_classes=["md-custom"], header_links=True)
29
+ app.render()
30
+ gr.Markdown(
31
+ """
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install gradio_mediagallery
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ from typing import Any, List
42
+ import gradio as gr
43
+ from gradio_folderexplorer import FolderExplorer
44
+ from gradio_folderexplorer.helpers import load_media_from_folder
45
+ from gradio_mediagallery import MediaGallery
46
+ from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
47
+ import os
48
+
49
+ # Configuration constant for the root directory containing media files
50
+ ROOT_DIR_PATH = "./src/examples"
51
+
52
+ def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
53
+ \"\"\"
54
+ Processes image metadata by calling the `transfer_metadata` helper.
55
+
56
+ Args:
57
+ image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
58
+
59
+ Returns:
60
+ List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
61
+ \"\"\"
62
+ if not image_data or not hasattr(image_data, "_data"):
63
+ return [gr.skip()] * len(output_fields)
64
+
65
+ return transfer_metadata(
66
+ output_fields=output_fields,
67
+ metadata=image_data._data,
68
+ remove_prefix_from_keys=True
69
+ )
70
+
71
+ # UI layout and logic
72
+ with gr.Blocks() as demo:
73
+ \"\"\"
74
+ A Gradio interface for browsing and displaying media files with metadata extraction.
75
+ \"\"\"
76
+ gr.Markdown("# MediaGallery with Metadata Extraction")
77
+ gr.Markdown(
78
+ \"\"\"
79
+ **To Test:**
80
+ 1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
81
+ 2. Click on an image in the **Media Gallery** to open the preview mode.
82
+ 3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
83
+ 4. Click the **'Load Metadata'** button inside the popup.
84
+ 5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
85
+ \"\"\"
86
+ )
87
+ with gr.Row(equal_height=True):
88
+ with gr.Column(scale=1, min_width=300):
89
+ folder_explorer = FolderExplorer(
90
+ label="Select a Folder",
91
+ root_dir=ROOT_DIR_PATH,
92
+ value=ROOT_DIR_PATH
93
+ )
94
+
95
+ with gr.Column(scale=3):
96
+ gallery = MediaGallery(
97
+ label="Media in Folder",
98
+ columns=6,
99
+ height="auto",
100
+ preview=False,
101
+ show_download_button=False,
102
+ only_custom_metadata=False,
103
+ popup_metadata_width="40%",
104
+ )
105
+
106
+ gr.Markdown("## Metadata Viewer")
107
+ with gr.Row():
108
+ model_box = gr.Textbox(label="Model")
109
+ fnumber_box = gr.Textbox(label="FNumber")
110
+ iso_box = gr.Textbox(label="ISOSpeedRatings")
111
+ s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
112
+ description_box = gr.Textbox(label="Description", lines=2)
113
+
114
+ # Event handling
115
+ output_fields = [
116
+ model_box,
117
+ fnumber_box,
118
+ iso_box,
119
+ s_churn,
120
+ description_box
121
+ ]
122
+
123
+ # Populate the gallery when the folder changes
124
+ folder_explorer.change(
125
+ fn=load_media_from_folder,
126
+ inputs=folder_explorer,
127
+ outputs=gallery
128
+ )
129
+
130
+ # Populate the gallery on initial load
131
+ demo.load(
132
+ fn=load_media_from_folder,
133
+ inputs=folder_explorer,
134
+ outputs=gallery
135
+ )
136
+
137
+ # Handle the load_metadata event from MediaGallery
138
+ gallery.load_metadata(
139
+ fn=handle_load_metadata,
140
+ inputs=None,
141
+ outputs=output_fields
142
+ )
143
+
144
+ if __name__ == "__main__":
145
+ \"\"\"
146
+ Launches the Gradio interface in debug mode.
147
+ \"\"\"
148
+ demo.launch(debug=True)
149
+ ```
150
+ """, elem_classes=["md-custom"], header_links=True)
151
+
152
+
153
+ gr.Markdown("""
154
+ ## `MediaGallery`
155
+
156
+ ### Initialization
157
+ """, elem_classes=["md-custom"], header_links=True)
158
+
159
+ gr.ParamViewer(value=_docs["MediaGallery"]["members"]["__init__"], linkify=[])
160
+
161
+
162
+ gr.Markdown("### Events")
163
+ gr.ParamViewer(value=_docs["MediaGallery"]["events"], linkify=['Event'])
164
+
165
+
166
+
167
+
168
+ gr.Markdown("""
169
+
170
+ ### User function
171
+
172
+ The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
173
+
174
+ - When used as an Input, the component only impacts the input signature of the user function.
175
+ - When used as an output, the component only impacts the return signature of the user function.
176
+
177
+ The code snippet below is accurate in cases where the component is used as both an input and an output.
178
+
179
+ - **As input:** Is passed, the preprocessed input data sent to the user's function in the backend.
180
+ - **As output:** Should return, the output data received by the component from the user's function in the backend.
181
+
182
+ ```python
183
+ def predict(
184
+ value: Any
185
+ ) -> list | None:
186
+ return value
187
+ ```
188
+ """, elem_classes=["md-custom", "MediaGallery-user-fn"], header_links=True)
189
+
190
+
191
+
192
+
193
+ demo.load(None, js=r"""function() {
194
+ const refs = {};
195
+ const user_fn_refs = {
196
+ MediaGallery: [], };
197
+ requestAnimationFrame(() => {
198
+
199
+ Object.entries(user_fn_refs).forEach(([key, refs]) => {
200
+ if (refs.length > 0) {
201
+ const el = document.querySelector(`.${key}-user-fn`);
202
+ if (!el) return;
203
+ refs.forEach(ref => {
204
+ el.innerHTML = el.innerHTML.replace(
205
+ new RegExp("\\b"+ref+"\\b", "g"),
206
+ `<a href="#h-${ref.toLowerCase()}">${ref}</a>`
207
+ );
208
+ })
209
+ }
210
+ })
211
+
212
+ Object.entries(refs).forEach(([key, refs]) => {
213
+ if (refs.length > 0) {
214
+ const el = document.querySelector(`.${key}`);
215
+ if (!el) return;
216
+ refs.forEach(ref => {
217
+ el.innerHTML = el.innerHTML.replace(
218
+ new RegExp("\\b"+ref+"\\b", "g"),
219
+ `<a href="#h-${ref.toLowerCase()}">${ref}</a>`
220
+ );
221
+ })
222
+ }
223
+ })
224
+ })
225
+ }
226
+
227
+ """)
228
+
229
+ demo.launch()
src/.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .eggs/
2
+ dist/
3
+ .vscode/
4
+ *.pyc
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ __tmp/*
9
+ *.pyi
10
+ .mypycache
11
+ .ruff_cache
12
+ node_modules
13
+ backend/**/templates/
src/.vscode/launch.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "name": "Python Debugger: Current File",
9
+ "type": "debugpy",
10
+ "request": "launch",
11
+ "program": "${file}",
12
+ "console": "integratedTerminal",
13
+ "justMyCode": false
14
+ },
15
+ {
16
+ "name": "Gradio dev (Python attach)",
17
+ "type": "debugpy",
18
+ "request": "attach",
19
+ "processId": "${command:pickProcess}",
20
+ "justMyCode": false
21
+ },
22
+ {
23
+ "name": "Gradio dev (Svelte attach)",
24
+ "type": "chrome",
25
+ "request": "attach",
26
+ "port": 9222,
27
+ }
28
+ ]
29
+ }
src/README.md ADDED
@@ -0,0 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags: [gradio-custom-component, gallery]
3
+ title: gradio_mediagallery
4
+ short_description: A gradio custom component
5
+ colorFrom: blue
6
+ colorTo: yellow
7
+ sdk: gradio
8
+ pinned: false
9
+ app_file: space.py
10
+ ---
11
+
12
+ # `gradio_mediagallery`
13
+ <img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20blue"> <a href="https://huggingface.co/spaces/elismasilva/gradio_mediagallery"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Demo-blue"></a><p><span>💻 <a href='https://github.com/DEVAIEXP/gradio_component_mediagallery'>Component GitHub Code</a></span></p>
14
+
15
+ Python library for easily interacting with trained machine learning models
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install gradio_mediagallery gradio_folderexplorer
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from typing import Any, List
27
+ import gradio as gr
28
+ from gradio_folderexplorer import FolderExplorer
29
+ from gradio_folderexplorer.helpers import load_media_from_folder
30
+ from gradio_mediagallery import MediaGallery
31
+ from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
32
+ import os
33
+
34
+ # Configuration constant for the root directory containing media files
35
+ ROOT_DIR_PATH = "./src/examples"
36
+
37
+ def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
38
+ """
39
+ Processes image metadata by calling the `transfer_metadata` helper.
40
+
41
+ Args:
42
+ image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
43
+
44
+ Returns:
45
+ List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
46
+ """
47
+ if not image_data or not hasattr(image_data, "_data"):
48
+ return [gr.skip()] * len(output_fields)
49
+
50
+ return transfer_metadata(
51
+ output_fields=output_fields,
52
+ metadata=image_data._data,
53
+ remove_prefix_from_keys=True
54
+ )
55
+
56
+ # UI layout and logic
57
+ with gr.Blocks() as demo:
58
+ """
59
+ A Gradio interface for browsing and displaying media files with metadata extraction.
60
+ """
61
+ gr.Markdown("# MediaGallery with Metadata Extraction")
62
+ gr.Markdown(
63
+ """
64
+ **To Test:**
65
+ 1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
66
+ 2. Click on an image in the **Media Gallery** to open the preview mode.
67
+ 3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
68
+ 4. Click the **'Load Metadata'** button inside the popup.
69
+ 5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
70
+ """
71
+ )
72
+ with gr.Row(equal_height=True):
73
+ with gr.Column(scale=1, min_width=300):
74
+ folder_explorer = FolderExplorer(
75
+ label="Select a Folder",
76
+ root_dir=ROOT_DIR_PATH,
77
+ value=ROOT_DIR_PATH
78
+ )
79
+
80
+ with gr.Column(scale=3):
81
+ gallery = MediaGallery(
82
+ label="Media in Folder",
83
+ columns=6,
84
+ height="auto",
85
+ preview=False,
86
+ show_download_button=False,
87
+ only_custom_metadata=False,
88
+ popup_metadata_width="40%",
89
+ )
90
+
91
+ gr.Markdown("## Metadata Viewer")
92
+ with gr.Row():
93
+ model_box = gr.Textbox(label="Model")
94
+ fnumber_box = gr.Textbox(label="FNumber")
95
+ iso_box = gr.Textbox(label="ISOSpeedRatings")
96
+ s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
97
+ description_box = gr.Textbox(label="Description", lines=2)
98
+
99
+ # Event handling
100
+ output_fields = [
101
+ model_box,
102
+ fnumber_box,
103
+ iso_box,
104
+ s_churn,
105
+ description_box
106
+ ]
107
+
108
+ # Populate the gallery when the folder changes
109
+ folder_explorer.change(
110
+ fn=load_media_from_folder,
111
+ inputs=folder_explorer,
112
+ outputs=gallery
113
+ )
114
+
115
+ # Populate the gallery on initial load
116
+ demo.load(
117
+ fn=load_media_from_folder,
118
+ inputs=folder_explorer,
119
+ outputs=gallery
120
+ )
121
+
122
+ # Handle the load_metadata event from MediaGallery
123
+ gallery.load_metadata(
124
+ fn=handle_load_metadata,
125
+ inputs=None,
126
+ outputs=output_fields
127
+ )
128
+
129
+ if __name__ == "__main__":
130
+ """
131
+ Launches the Gradio interface in debug mode.
132
+ """
133
+ demo.launch(debug=True)
134
+ ```
135
+
136
+ ## `MediaGallery`
137
+
138
+ ### Initialization
139
+
140
+ <table>
141
+ <thead>
142
+ <tr>
143
+ <th align="left">name</th>
144
+ <th align="left" style="width: 25%;">type</th>
145
+ <th align="left">default</th>
146
+ <th align="left">description</th>
147
+ </tr>
148
+ </thead>
149
+ <tbody>
150
+ <tr>
151
+ <td align="left"><code>value</code></td>
152
+ <td align="left" style="width: 25%;">
153
+
154
+ ```python
155
+ Sequence[
156
+ np.ndarray | PIL.Image.Image | str | Path | tuple
157
+ ]
158
+ | Callable
159
+ | None
160
+ ```
161
+
162
+ </td>
163
+ <td align="left"><code>None</code></td>
164
+ <td align="left">List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.</td>
165
+ </tr>
166
+
167
+ <tr>
168
+ <td align="left"><code>file_types</code></td>
169
+ <td align="left" style="width: 25%;">
170
+
171
+ ```python
172
+ list[str] | None
173
+ ```
174
+
175
+ </td>
176
+ <td align="left"><code>None</code></td>
177
+ <td align="left">List of file extensions or types of files to be uploaded (e.g. ['image', '.mp4']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.</td>
178
+ </tr>
179
+
180
+ <tr>
181
+ <td align="left"><code>label</code></td>
182
+ <td align="left" style="width: 25%;">
183
+
184
+ ```python
185
+ str | I18nData | None
186
+ ```
187
+
188
+ </td>
189
+ <td align="left"><code>None</code></td>
190
+ <td align="left">the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td>
191
+ </tr>
192
+
193
+ <tr>
194
+ <td align="left"><code>every</code></td>
195
+ <td align="left" style="width: 25%;">
196
+
197
+ ```python
198
+ Timer | float | None
199
+ ```
200
+
201
+ </td>
202
+ <td align="left"><code>None</code></td>
203
+ <td align="left">Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.</td>
204
+ </tr>
205
+
206
+ <tr>
207
+ <td align="left"><code>inputs</code></td>
208
+ <td align="left" style="width: 25%;">
209
+
210
+ ```python
211
+ Component | Sequence[Component] | set[Component] | None
212
+ ```
213
+
214
+ </td>
215
+ <td align="left"><code>None</code></td>
216
+ <td align="left">Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.</td>
217
+ </tr>
218
+
219
+ <tr>
220
+ <td align="left"><code>show_label</code></td>
221
+ <td align="left" style="width: 25%;">
222
+
223
+ ```python
224
+ bool | None
225
+ ```
226
+
227
+ </td>
228
+ <td align="left"><code>None</code></td>
229
+ <td align="left">if True, will display label.</td>
230
+ </tr>
231
+
232
+ <tr>
233
+ <td align="left"><code>container</code></td>
234
+ <td align="left" style="width: 25%;">
235
+
236
+ ```python
237
+ bool
238
+ ```
239
+
240
+ </td>
241
+ <td align="left"><code>True</code></td>
242
+ <td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
243
+ </tr>
244
+
245
+ <tr>
246
+ <td align="left"><code>scale</code></td>
247
+ <td align="left" style="width: 25%;">
248
+
249
+ ```python
250
+ int | None
251
+ ```
252
+
253
+ </td>
254
+ <td align="left"><code>None</code></td>
255
+ <td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
256
+ </tr>
257
+
258
+ <tr>
259
+ <td align="left"><code>min_width</code></td>
260
+ <td align="left" style="width: 25%;">
261
+
262
+ ```python
263
+ int
264
+ ```
265
+
266
+ </td>
267
+ <td align="left"><code>160</code></td>
268
+ <td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
269
+ </tr>
270
+
271
+ <tr>
272
+ <td align="left"><code>visible</code></td>
273
+ <td align="left" style="width: 25%;">
274
+
275
+ ```python
276
+ bool | Literal["hidden"]
277
+ ```
278
+
279
+ </td>
280
+ <td align="left"><code>True</code></td>
281
+ <td align="left">If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM</td>
282
+ </tr>
283
+
284
+ <tr>
285
+ <td align="left"><code>elem_id</code></td>
286
+ <td align="left" style="width: 25%;">
287
+
288
+ ```python
289
+ str | None
290
+ ```
291
+
292
+ </td>
293
+ <td align="left"><code>None</code></td>
294
+ <td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
295
+ </tr>
296
+
297
+ <tr>
298
+ <td align="left"><code>elem_classes</code></td>
299
+ <td align="left" style="width: 25%;">
300
+
301
+ ```python
302
+ list[str] | str | None
303
+ ```
304
+
305
+ </td>
306
+ <td align="left"><code>None</code></td>
307
+ <td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
308
+ </tr>
309
+
310
+ <tr>
311
+ <td align="left"><code>render</code></td>
312
+ <td align="left" style="width: 25%;">
313
+
314
+ ```python
315
+ bool
316
+ ```
317
+
318
+ </td>
319
+ <td align="left"><code>True</code></td>
320
+ <td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
321
+ </tr>
322
+
323
+ <tr>
324
+ <td align="left"><code>key</code></td>
325
+ <td align="left" style="width: 25%;">
326
+
327
+ ```python
328
+ int | str | tuple[int | str, ...] | None
329
+ ```
330
+
331
+ </td>
332
+ <td align="left"><code>None</code></td>
333
+ <td align="left">in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.</td>
334
+ </tr>
335
+
336
+ <tr>
337
+ <td align="left"><code>preserved_by_key</code></td>
338
+ <td align="left" style="width: 25%;">
339
+
340
+ ```python
341
+ list[str] | str | None
342
+ ```
343
+
344
+ </td>
345
+ <td align="left"><code>"value"</code></td>
346
+ <td align="left">A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.</td>
347
+ </tr>
348
+
349
+ <tr>
350
+ <td align="left"><code>columns</code></td>
351
+ <td align="left" style="width: 25%;">
352
+
353
+ ```python
354
+ int | None
355
+ ```
356
+
357
+ </td>
358
+ <td align="left"><code>2</code></td>
359
+ <td align="left">Represents the number of images that should be shown in one row.</td>
360
+ </tr>
361
+
362
+ <tr>
363
+ <td align="left"><code>rows</code></td>
364
+ <td align="left" style="width: 25%;">
365
+
366
+ ```python
367
+ int | None
368
+ ```
369
+
370
+ </td>
371
+ <td align="left"><code>None</code></td>
372
+ <td align="left">Represents the number of rows in the image grid.</td>
373
+ </tr>
374
+
375
+ <tr>
376
+ <td align="left"><code>height</code></td>
377
+ <td align="left" style="width: 25%;">
378
+
379
+ ```python
380
+ int | float | str | None
381
+ ```
382
+
383
+ </td>
384
+ <td align="left"><code>None</code></td>
385
+ <td align="left">The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.</td>
386
+ </tr>
387
+
388
+ <tr>
389
+ <td align="left"><code>allow_preview</code></td>
390
+ <td align="left" style="width: 25%;">
391
+
392
+ ```python
393
+ bool
394
+ ```
395
+
396
+ </td>
397
+ <td align="left"><code>True</code></td>
398
+ <td align="left">If True, images in the gallery will be enlarged when they are clicked. Default is True.</td>
399
+ </tr>
400
+
401
+ <tr>
402
+ <td align="left"><code>preview</code></td>
403
+ <td align="left" style="width: 25%;">
404
+
405
+ ```python
406
+ bool | None
407
+ ```
408
+
409
+ </td>
410
+ <td align="left"><code>None</code></td>
411
+ <td align="left">If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.</td>
412
+ </tr>
413
+
414
+ <tr>
415
+ <td align="left"><code>selected_index</code></td>
416
+ <td align="left" style="width: 25%;">
417
+
418
+ ```python
419
+ int | None
420
+ ```
421
+
422
+ </td>
423
+ <td align="left"><code>None</code></td>
424
+ <td align="left">The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.</td>
425
+ </tr>
426
+
427
+ <tr>
428
+ <td align="left"><code>object_fit</code></td>
429
+ <td align="left" style="width: 25%;">
430
+
431
+ ```python
432
+ Literal[
433
+ "contain", "cover", "fill", "none", "scale-down"
434
+ ]
435
+ | None
436
+ ```
437
+
438
+ </td>
439
+ <td align="left"><code>None</code></td>
440
+ <td align="left">CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".</td>
441
+ </tr>
442
+
443
+ <tr>
444
+ <td align="left"><code>show_share_button</code></td>
445
+ <td align="left" style="width: 25%;">
446
+
447
+ ```python
448
+ bool | None
449
+ ```
450
+
451
+ </td>
452
+ <td align="left"><code>None</code></td>
453
+ <td align="left">If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
454
+ </tr>
455
+
456
+ <tr>
457
+ <td align="left"><code>show_download_button</code></td>
458
+ <td align="left" style="width: 25%;">
459
+
460
+ ```python
461
+ bool | None
462
+ ```
463
+
464
+ </td>
465
+ <td align="left"><code>True</code></td>
466
+ <td align="left">If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.</td>
467
+ </tr>
468
+
469
+ <tr>
470
+ <td align="left"><code>interactive</code></td>
471
+ <td align="left" style="width: 25%;">
472
+
473
+ ```python
474
+ bool | None
475
+ ```
476
+
477
+ </td>
478
+ <td align="left"><code>None</code></td>
479
+ <td align="left">If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.</td>
480
+ </tr>
481
+
482
+ <tr>
483
+ <td align="left"><code>type</code></td>
484
+ <td align="left" style="width: 25%;">
485
+
486
+ ```python
487
+ Literal["numpy", "pil", "filepath"]
488
+ ```
489
+
490
+ </td>
491
+ <td align="left"><code>"filepath"</code></td>
492
+ <td align="left">The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.</td>
493
+ </tr>
494
+
495
+ <tr>
496
+ <td align="left"><code>show_fullscreen_button</code></td>
497
+ <td align="left" style="width: 25%;">
498
+
499
+ ```python
500
+ bool
501
+ ```
502
+
503
+ </td>
504
+ <td align="left"><code>True</code></td>
505
+ <td align="left">If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
506
+ </tr>
507
+
508
+ <tr>
509
+ <td align="left"><code>only_custom_metadata</code></td>
510
+ <td align="left" style="width: 25%;">
511
+
512
+ ```python
513
+ bool
514
+ ```
515
+
516
+ </td>
517
+ <td align="left"><code>True</code></td>
518
+ <td align="left">If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.</td>
519
+ </tr>
520
+
521
+ <tr>
522
+ <td align="left"><code>popup_metadata_width</code></td>
523
+ <td align="left" style="width: 25%;">
524
+
525
+ ```python
526
+ int | str
527
+ ```
528
+
529
+ </td>
530
+ <td align="left"><code>500</code></td>
531
+ <td align="left">The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").</td>
532
+ </tr>
533
+ </tbody></table>
534
+
535
+
536
+ ### Events
537
+
538
+ | name | description |
539
+ |:-----|:------------|
540
+ | `select` | Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data |
541
+ | `change` | Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. |
542
+ | `delete` | This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data |
543
+ | `preview_close` | This event is triggered when the MediaGallery preview is closed by the user |
544
+ | `preview_open` | This event is triggered when the MediaGallery preview is opened by the user |
545
+ | `load_metadata` | Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata. |
546
+
547
+
548
+
549
+ ### User function
550
+
551
+ The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
552
+
553
+ - When used as an Input, the component only impacts the input signature of the user function.
554
+ - When used as an output, the component only impacts the return signature of the user function.
555
+
556
+ The code snippet below is accurate in cases where the component is used as both an input and an output.
557
+
558
+ - **As output:** Is passed, the preprocessed input data sent to the user's function in the backend.
559
+ - **As input:** Should return, the output data received by the component from the user's function in the backend.
560
+
561
+ ```python
562
+ def predict(
563
+ value: Any
564
+ ) -> list | None:
565
+ return value
566
+ ```
567
+
src/README_TEMPLATE.md ADDED
@@ -0,0 +1,565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags: [gradio-custom-component, gallery]
3
+ title: gradio_mediagallery
4
+ short_description: A gradio custom component
5
+ colorFrom: blue
6
+ colorTo: yellow
7
+ sdk: gradio
8
+ pinned: false
9
+ app_file: space.py
10
+ ---
11
+
12
+ # `gradio_mediagallery`
13
+ <img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20blue"> <a href="https://huggingface.co/spaces/elismasilva/gradio_mediagallery"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Demo-blue"></a><p><span>💻 <a href='https://github.com/DEVAIEXP/gradio_component_mediagallery'>Component GitHub Code</a></span></p>
14
+
15
+ Python library for easily interacting with trained machine learning models
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install gradio_mediagallery gradio_folderexplorer
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from typing import Any, List
27
+ import gradio as gr
28
+ from gradio_folderexplorer import FolderExplorer
29
+ from gradio_folderexplorer.helpers import load_media_from_folder
30
+ from gradio_mediagallery import MediaGallery
31
+ from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
32
+ import os
33
+
34
+
35
+
36
+ # --- Configuration Constants ---
37
+ ROOT_DIR_PATH = "./examples" # Use uma pasta com imagens com metadados para teste
38
+
39
+ # --- Event Callback Function ---
40
+
41
+ # Esta função é chamada quando o evento `load_metadata` é disparado do frontend.
42
+ def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
43
+ """
44
+ Processes image metadata by calling the agnostic `transfer_metadata` helper.
45
+ """
46
+ if not image_data or not hasattr(image_data, "_data"):
47
+ return [gr.skip()] * len(output_fields)
48
+
49
+ # Call the agnostic helper function to do the heavy lifting.
50
+ return transfer_metadata(
51
+ output_fields=output_fields,
52
+ metadata=image_data._data,
53
+ remove_prefix_from_keys=True
54
+ )
55
+
56
+ # --- UI Layout and Logic ---
57
+
58
+ with gr.Blocks() as demo:
59
+ gr.Markdown("# MediaGallery with Metadata Extraction")
60
+ gr.Markdown(
61
+ """
62
+ **To Test:**
63
+ 1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
64
+ 2. Click on an image in the **Media Gallery** to open the preview mode.
65
+ 3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
66
+ 4. Click the **'Load Metadata'** button inside the popup.
67
+ 5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
68
+ """
69
+ )
70
+ with gr.Row(equal_height=True):
71
+ with gr.Column(scale=1, min_width=300):
72
+ folder_explorer = FolderExplorer(
73
+ label="Select a Folder",
74
+ root_dir=ROOT_DIR_PATH,
75
+ value=ROOT_DIR_PATH
76
+ )
77
+
78
+ with gr.Column(scale=3):
79
+ # Usando nosso MediaGallery customizado
80
+ gallery = MediaGallery(
81
+ label="Media in Folder",
82
+ columns=6,
83
+ height="auto",
84
+ preview=False,
85
+ show_download_button=False,
86
+ only_custom_metadata=False, # Agora mostra todos os metadados
87
+ popup_metadata_width="40%", # Popup mais largo
88
+
89
+ )
90
+
91
+ gr.Markdown("## Metadata Viewer")
92
+ with gr.Row():
93
+ model_box = gr.Textbox(label="Model")
94
+ fnumber_box = gr.Textbox(label="FNumber")
95
+ iso_box = gr.Textbox(label="ISOSpeedRatings")
96
+ s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
97
+ description_box = gr.Textbox(label="Description", lines=2)
98
+ # --- Event Handling ---
99
+
100
+ # Evento para popular a galeria quando a pasta muda
101
+ folder_explorer.change(
102
+ fn=load_media_from_folder,
103
+ inputs=folder_explorer,
104
+ outputs=gallery
105
+ )
106
+
107
+ # Evento para popular a galeria no carregamento inicial
108
+ demo.load(
109
+ fn=load_media_from_folder,
110
+ inputs=folder_explorer,
111
+ outputs=gallery
112
+ )
113
+
114
+ output_fields = [
115
+ model_box,
116
+ fnumber_box,
117
+ iso_box,
118
+ s_churn,
119
+ description_box
120
+ ]
121
+
122
+ # --- NOVO EVENTO DE METADADOS ---
123
+ # Liga o evento `load_metadata` do nosso MediaGallery à função de callback.
124
+ gallery.load_metadata(
125
+ fn=handle_load_metadata,
126
+ inputs=None, # O dado vem do payload do evento, não de um input explícito.
127
+ outputs=output_fields
128
+ )
129
+
130
+ if __name__ == "__main__":
131
+ demo.launch(debug=True)
132
+ ```
133
+
134
+ ## `MediaGallery`
135
+
136
+ ### Initialization
137
+
138
+ <table>
139
+ <thead>
140
+ <tr>
141
+ <th align="left">name</th>
142
+ <th align="left" style="width: 25%;">type</th>
143
+ <th align="left">default</th>
144
+ <th align="left">description</th>
145
+ </tr>
146
+ </thead>
147
+ <tbody>
148
+ <tr>
149
+ <td align="left"><code>value</code></td>
150
+ <td align="left" style="width: 25%;">
151
+
152
+ ```python
153
+ Sequence[
154
+ np.ndarray | PIL.Image.Image | str | Path | tuple
155
+ ]
156
+ | Callable
157
+ | None
158
+ ```
159
+
160
+ </td>
161
+ <td align="left"><code>None</code></td>
162
+ <td align="left">List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.</td>
163
+ </tr>
164
+
165
+ <tr>
166
+ <td align="left"><code>file_types</code></td>
167
+ <td align="left" style="width: 25%;">
168
+
169
+ ```python
170
+ list[str] | None
171
+ ```
172
+
173
+ </td>
174
+ <td align="left"><code>None</code></td>
175
+ <td align="left">List of file extensions or types of files to be uploaded (e.g. ['image', '.mp4']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.</td>
176
+ </tr>
177
+
178
+ <tr>
179
+ <td align="left"><code>label</code></td>
180
+ <td align="left" style="width: 25%;">
181
+
182
+ ```python
183
+ str | I18nData | None
184
+ ```
185
+
186
+ </td>
187
+ <td align="left"><code>None</code></td>
188
+ <td align="left">the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td>
189
+ </tr>
190
+
191
+ <tr>
192
+ <td align="left"><code>every</code></td>
193
+ <td align="left" style="width: 25%;">
194
+
195
+ ```python
196
+ Timer | float | None
197
+ ```
198
+
199
+ </td>
200
+ <td align="left"><code>None</code></td>
201
+ <td align="left">Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.</td>
202
+ </tr>
203
+
204
+ <tr>
205
+ <td align="left"><code>inputs</code></td>
206
+ <td align="left" style="width: 25%;">
207
+
208
+ ```python
209
+ Component | Sequence[Component] | set[Component] | None
210
+ ```
211
+
212
+ </td>
213
+ <td align="left"><code>None</code></td>
214
+ <td align="left">Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.</td>
215
+ </tr>
216
+
217
+ <tr>
218
+ <td align="left"><code>show_label</code></td>
219
+ <td align="left" style="width: 25%;">
220
+
221
+ ```python
222
+ bool | None
223
+ ```
224
+
225
+ </td>
226
+ <td align="left"><code>None</code></td>
227
+ <td align="left">if True, will display label.</td>
228
+ </tr>
229
+
230
+ <tr>
231
+ <td align="left"><code>container</code></td>
232
+ <td align="left" style="width: 25%;">
233
+
234
+ ```python
235
+ bool
236
+ ```
237
+
238
+ </td>
239
+ <td align="left"><code>True</code></td>
240
+ <td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
241
+ </tr>
242
+
243
+ <tr>
244
+ <td align="left"><code>scale</code></td>
245
+ <td align="left" style="width: 25%;">
246
+
247
+ ```python
248
+ int | None
249
+ ```
250
+
251
+ </td>
252
+ <td align="left"><code>None</code></td>
253
+ <td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
254
+ </tr>
255
+
256
+ <tr>
257
+ <td align="left"><code>min_width</code></td>
258
+ <td align="left" style="width: 25%;">
259
+
260
+ ```python
261
+ int
262
+ ```
263
+
264
+ </td>
265
+ <td align="left"><code>160</code></td>
266
+ <td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
267
+ </tr>
268
+
269
+ <tr>
270
+ <td align="left"><code>visible</code></td>
271
+ <td align="left" style="width: 25%;">
272
+
273
+ ```python
274
+ bool | Literal["hidden"]
275
+ ```
276
+
277
+ </td>
278
+ <td align="left"><code>True</code></td>
279
+ <td align="left">If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM</td>
280
+ </tr>
281
+
282
+ <tr>
283
+ <td align="left"><code>elem_id</code></td>
284
+ <td align="left" style="width: 25%;">
285
+
286
+ ```python
287
+ str | None
288
+ ```
289
+
290
+ </td>
291
+ <td align="left"><code>None</code></td>
292
+ <td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
293
+ </tr>
294
+
295
+ <tr>
296
+ <td align="left"><code>elem_classes</code></td>
297
+ <td align="left" style="width: 25%;">
298
+
299
+ ```python
300
+ list[str] | str | None
301
+ ```
302
+
303
+ </td>
304
+ <td align="left"><code>None</code></td>
305
+ <td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
306
+ </tr>
307
+
308
+ <tr>
309
+ <td align="left"><code>render</code></td>
310
+ <td align="left" style="width: 25%;">
311
+
312
+ ```python
313
+ bool
314
+ ```
315
+
316
+ </td>
317
+ <td align="left"><code>True</code></td>
318
+ <td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
319
+ </tr>
320
+
321
+ <tr>
322
+ <td align="left"><code>key</code></td>
323
+ <td align="left" style="width: 25%;">
324
+
325
+ ```python
326
+ int | str | tuple[int | str, ...] | None
327
+ ```
328
+
329
+ </td>
330
+ <td align="left"><code>None</code></td>
331
+ <td align="left">in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.</td>
332
+ </tr>
333
+
334
+ <tr>
335
+ <td align="left"><code>preserved_by_key</code></td>
336
+ <td align="left" style="width: 25%;">
337
+
338
+ ```python
339
+ list[str] | str | None
340
+ ```
341
+
342
+ </td>
343
+ <td align="left"><code>"value"</code></td>
344
+ <td align="left">A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.</td>
345
+ </tr>
346
+
347
+ <tr>
348
+ <td align="left"><code>columns</code></td>
349
+ <td align="left" style="width: 25%;">
350
+
351
+ ```python
352
+ int | None
353
+ ```
354
+
355
+ </td>
356
+ <td align="left"><code>2</code></td>
357
+ <td align="left">Represents the number of images that should be shown in one row.</td>
358
+ </tr>
359
+
360
+ <tr>
361
+ <td align="left"><code>rows</code></td>
362
+ <td align="left" style="width: 25%;">
363
+
364
+ ```python
365
+ int | None
366
+ ```
367
+
368
+ </td>
369
+ <td align="left"><code>None</code></td>
370
+ <td align="left">Represents the number of rows in the image grid.</td>
371
+ </tr>
372
+
373
+ <tr>
374
+ <td align="left"><code>height</code></td>
375
+ <td align="left" style="width: 25%;">
376
+
377
+ ```python
378
+ int | float | str | None
379
+ ```
380
+
381
+ </td>
382
+ <td align="left"><code>None</code></td>
383
+ <td align="left">The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.</td>
384
+ </tr>
385
+
386
+ <tr>
387
+ <td align="left"><code>allow_preview</code></td>
388
+ <td align="left" style="width: 25%;">
389
+
390
+ ```python
391
+ bool
392
+ ```
393
+
394
+ </td>
395
+ <td align="left"><code>True</code></td>
396
+ <td align="left">If True, images in the gallery will be enlarged when they are clicked. Default is True.</td>
397
+ </tr>
398
+
399
+ <tr>
400
+ <td align="left"><code>preview</code></td>
401
+ <td align="left" style="width: 25%;">
402
+
403
+ ```python
404
+ bool | None
405
+ ```
406
+
407
+ </td>
408
+ <td align="left"><code>None</code></td>
409
+ <td align="left">If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.</td>
410
+ </tr>
411
+
412
+ <tr>
413
+ <td align="left"><code>selected_index</code></td>
414
+ <td align="left" style="width: 25%;">
415
+
416
+ ```python
417
+ int | None
418
+ ```
419
+
420
+ </td>
421
+ <td align="left"><code>None</code></td>
422
+ <td align="left">The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.</td>
423
+ </tr>
424
+
425
+ <tr>
426
+ <td align="left"><code>object_fit</code></td>
427
+ <td align="left" style="width: 25%;">
428
+
429
+ ```python
430
+ Literal[
431
+ "contain", "cover", "fill", "none", "scale-down"
432
+ ]
433
+ | None
434
+ ```
435
+
436
+ </td>
437
+ <td align="left"><code>None</code></td>
438
+ <td align="left">CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".</td>
439
+ </tr>
440
+
441
+ <tr>
442
+ <td align="left"><code>show_share_button</code></td>
443
+ <td align="left" style="width: 25%;">
444
+
445
+ ```python
446
+ bool | None
447
+ ```
448
+
449
+ </td>
450
+ <td align="left"><code>None</code></td>
451
+ <td align="left">If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
452
+ </tr>
453
+
454
+ <tr>
455
+ <td align="left"><code>show_download_button</code></td>
456
+ <td align="left" style="width: 25%;">
457
+
458
+ ```python
459
+ bool | None
460
+ ```
461
+
462
+ </td>
463
+ <td align="left"><code>True</code></td>
464
+ <td align="left">If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.</td>
465
+ </tr>
466
+
467
+ <tr>
468
+ <td align="left"><code>interactive</code></td>
469
+ <td align="left" style="width: 25%;">
470
+
471
+ ```python
472
+ bool | None
473
+ ```
474
+
475
+ </td>
476
+ <td align="left"><code>None</code></td>
477
+ <td align="left">If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.</td>
478
+ </tr>
479
+
480
+ <tr>
481
+ <td align="left"><code>type</code></td>
482
+ <td align="left" style="width: 25%;">
483
+
484
+ ```python
485
+ Literal["numpy", "pil", "filepath"]
486
+ ```
487
+
488
+ </td>
489
+ <td align="left"><code>"filepath"</code></td>
490
+ <td align="left">The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.</td>
491
+ </tr>
492
+
493
+ <tr>
494
+ <td align="left"><code>show_fullscreen_button</code></td>
495
+ <td align="left" style="width: 25%;">
496
+
497
+ ```python
498
+ bool
499
+ ```
500
+
501
+ </td>
502
+ <td align="left"><code>True</code></td>
503
+ <td align="left">If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
504
+ </tr>
505
+
506
+ <tr>
507
+ <td align="left"><code>only_custom_metadata</code></td>
508
+ <td align="left" style="width: 25%;">
509
+
510
+ ```python
511
+ bool
512
+ ```
513
+
514
+ </td>
515
+ <td align="left"><code>True</code></td>
516
+ <td align="left">If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.</td>
517
+ </tr>
518
+
519
+ <tr>
520
+ <td align="left"><code>popup_metadata_width</code></td>
521
+ <td align="left" style="width: 25%;">
522
+
523
+ ```python
524
+ int | str
525
+ ```
526
+
527
+ </td>
528
+ <td align="left"><code>500</code></td>
529
+ <td align="left">The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").</td>
530
+ </tr>
531
+ </tbody></table>
532
+
533
+
534
+ ### Events
535
+
536
+ | name | description |
537
+ |:-----|:------------|
538
+ | `select` | Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data |
539
+ | `change` | Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. |
540
+ | `delete` | This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data |
541
+ | `preview_close` | This event is triggered when the MediaGallery preview is closed by the user |
542
+ | `preview_open` | This event is triggered when the MediaGallery preview is opened by the user |
543
+ | `load_metadata` | Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata. |
544
+
545
+
546
+
547
+ ### User function
548
+
549
+ The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
550
+
551
+ - When used as an Input, the component only impacts the input signature of the user function.
552
+ - When used as an output, the component only impacts the return signature of the user function.
553
+
554
+ The code snippet below is accurate in cases where the component is used as both an input and an output.
555
+
556
+ - **As output:** Is passed, the preprocessed input data sent to the user's function in the backend.
557
+ - **As input:** Should return, the output data received by the component from the user's function in the backend.
558
+
559
+ ```python
560
+ def predict(
561
+ value: Any
562
+ ) -> list | None:
563
+ return value
564
+ ```
565
+
src/backend/gradio_mediagallery/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+
2
+ from .mediagallery import MediaGallery
3
+
4
+ __all__ = ['MediaGallery']
src/backend/gradio_mediagallery/helpers.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import fields, is_dataclass
2
+ import os
3
+ import numpy as np
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional, Type
6
+ from PIL import Image, PngImagePlugin, ExifTags
7
+
8
+ def infer_type(s: str):
9
+ """
10
+ Infers and converts a string to the most likely data type.
11
+
12
+ It attempts conversions in the following order:
13
+ 1. Integer
14
+ 2. Float
15
+ 3. Boolean (case-insensitive 'true' or 'false')
16
+ If all conversions fail, it returns the original string.
17
+
18
+ Args:
19
+ s: The input string to be converted.
20
+
21
+ Returns:
22
+ The converted value (int, float, bool) or the original string.
23
+ """
24
+ if not isinstance(s, str):
25
+ # If the input is not a string, return it as is.
26
+ return s
27
+
28
+ # 1. Try to convert to an integer
29
+ try:
30
+ return int(s)
31
+ except ValueError:
32
+ # Not an integer, continue...
33
+ pass
34
+
35
+ # 2. Try to convert to a float
36
+ try:
37
+ return float(s)
38
+ except ValueError:
39
+ # Not a float, continue...
40
+ pass
41
+
42
+ # 3. Check for a boolean value
43
+ # This explicit check is important because bool('False') evaluates to True.
44
+ s_lower = s.lower()
45
+ if s_lower == 'true':
46
+ return True
47
+ if s_lower == 'false':
48
+ return False
49
+
50
+ # 4. If nothing else worked, return the original string
51
+ return s
52
+
53
+ def build_path_to_metadata_key_map(cls: Type, prefix_list: List[str]) -> Dict[str, str]:
54
+ """
55
+ Builds a map from a dataclass field path (e.g., 'image_settings.model') to the
56
+ expected key in the metadata dictionary (e.g., 'Image Settings - Model').
57
+ """
58
+ path_map = {}
59
+ if not is_dataclass(cls):
60
+ return {}
61
+
62
+ for f in fields(cls):
63
+ current_path = f.name
64
+
65
+ if is_dataclass(f.type):
66
+ parent_label = f.metadata.get("label", f.name.replace("_", " ").title())
67
+ new_prefix_list = prefix_list + [parent_label]
68
+ nested_map = build_path_to_metadata_key_map(f.type, new_prefix_list)
69
+ for nested_path, metadata_key in nested_map.items():
70
+ path_map[f"{current_path}.{nested_path}"] = metadata_key
71
+ else:
72
+ label = f.metadata.get("label", f.name.replace("_", " ").title())
73
+ full_prefix = " - ".join(prefix_list)
74
+ metadata_key = f"{full_prefix} - {label}" if full_prefix else label
75
+ path_map[current_path] = metadata_key
76
+
77
+ return path_map
78
+
79
+ def extract_metadata(image_data: str | Path | Image.Image | np.ndarray | None, only_custom_metadata: bool = True) -> Dict[str, Any]:
80
+ """
81
+ Extracts metadata from an image.
82
+
83
+ Args:
84
+ image_data: Image data as a filepath, Path, PIL Image, NumPy array, or None.
85
+ only_custom_metadata: If True, excludes technical metadata (e.g., ImageWidth, ImageHeight). Defaults to True.
86
+
87
+ Returns:
88
+ Dictionary of extracted metadata. Returns empty dictionary if no metadata is available or extraction fails.
89
+ """
90
+ if not image_data:
91
+ return {}
92
+
93
+ try:
94
+ # Convert image_data to PIL.Image
95
+ if isinstance(image_data, (str, Path)):
96
+ image = Image.open(image_data)
97
+ elif isinstance(image_data, np.ndarray):
98
+ image = Image.fromarray(image_data)
99
+ elif isinstance(image_data, Image.Image):
100
+ image = image_data
101
+ elif hasattr(image_data, 'path'): # For ImageMetaData
102
+ image = Image.open(image_data.path)
103
+ else:
104
+ return {}
105
+
106
+ decoded_meta = {}
107
+ if image.format == "PNG":
108
+ if not only_custom_metadata:
109
+ decoded_meta["ImageWidth"] = image.width
110
+ decoded_meta["ImageHeight"] = image.height
111
+ metadata = image.info
112
+ if metadata:
113
+ for key, value in metadata.items():
114
+ if isinstance(value, bytes):
115
+ value = value.decode(errors='ignore')
116
+ decoded_meta[str(key)] = value
117
+ else:
118
+ exif_data = image.getexif()
119
+ if exif_data:
120
+ for tag_id, value in exif_data.items():
121
+ tag = ExifTags.TAGS.get(tag_id, tag_id)
122
+ if isinstance(value, bytes):
123
+ value = value.decode(errors='ignore')
124
+ decoded_meta[str(tag)] = value
125
+ if not only_custom_metadata:
126
+ decoded_meta["ImageWidth"] = image.width
127
+ decoded_meta["ImageHeight"] = image.height
128
+
129
+ return decoded_meta
130
+ except Exception:
131
+ return {}
132
+
133
+ def add_metadata(image_data: str | Path | Image.Image | np.ndarray, save_path: str, metadata: Dict[str, Any]) -> bool:
134
+ """
135
+ Adds metadata to an image and saves it to the specified path.
136
+
137
+ Args:
138
+ image_data: Image data as a filepath, Path, PIL Image, or NumPy array.
139
+ save_path: Filepath where the modified image will be saved.
140
+ metadata: Dictionary of metadata to add to the image.
141
+
142
+ Returns:
143
+ True if metadata was added and image was saved successfully, False otherwise.
144
+ """
145
+ try:
146
+ if not bool(save_path):
147
+ return False
148
+
149
+ # Convert image_data to PIL.Image
150
+ if isinstance(image_data, (str, Path)):
151
+ image = Image.open(image_data)
152
+ elif isinstance(image_data, np.ndarray):
153
+ image = Image.fromarray(image_data)
154
+ elif isinstance(image_data, Image.Image):
155
+ image = image_data
156
+ elif hasattr(image_data, 'path'): # For ImageMetaData
157
+ image = Image.open(image_data.path)
158
+ else:
159
+ return False
160
+
161
+ _, ext = os.path.splitext(save_path)
162
+ image_copy = image.copy()
163
+
164
+ if (image.format if image.format is not None else ext.replace('.','').upper()) == "PNG":
165
+ meta = None
166
+ if metadata:
167
+ meta = PngImagePlugin.PngInfo()
168
+ for key, value in metadata.items():
169
+ meta.add_text(str(key), str(value))
170
+ image_copy.info.update(metadata) # For reference, but requires pnginfo when saving
171
+ image_copy.save(save_path, pnginfo=meta)
172
+ else:
173
+ if metadata:
174
+ exif = image_copy.getexif() or Image.Exif()
175
+ for key, value in metadata.items():
176
+ tag_id = next((k for k, v in ExifTags.TAGS.items() if v == key), None)
177
+ if tag_id:
178
+ exif[tag_id] = value
179
+ image_copy.exif = exif
180
+ image_copy.save(save_path)
181
+ return True
182
+ except Exception:
183
+ return False
184
+
185
+ def transfer_metadata(
186
+ output_fields: List[Any],
187
+ metadata: Dict[str, Any],
188
+ propertysheet_map: Optional[Dict[int, Dict[str, Any]]] = None,
189
+ gradio_component_map: Optional[Dict[int, str]] = None,
190
+ remove_prefix_from_keys: bool = False
191
+ ) -> List[Any]:
192
+ """
193
+ Maps a flat metadata dictionary to a list of Gradio UI components, with
194
+ flexible and powerful matching logic for all component types.
195
+
196
+ This function is UI-agnostic. For PropertySheets, it uses the `propertysheet_map`
197
+ to reconstruct nested dataclass instances. It intelligently builds the expected
198
+ metadata keys by interpreting the `prefixes` list: strings found as keys in the
199
+ `metadata` are replaced by their values, while other strings are used literally.
200
+
201
+ For standard Gradio components, it uses a three-tiered priority system:
202
+ 1. **Explicit Mapping (Highest Priority):** If `gradio_component_map` is provided,
203
+ it uses the specified metadata key for a given component ID.
204
+ 2. **Base Label Matching:** If `remove_prefix_from_keys=True` and no explicit map
205
+ is found, it matches the component's `.label` against "base" (prefix-stripped)
206
+ metadata keys.
207
+ 3. **Exact Label Matching (Default):** Otherwise, it attempts an exact match
208
+ between the component's `.label` and a metadata key.
209
+
210
+ Args:
211
+ output_fields (List[Any]): The list of Gradio components to be updated.
212
+ metadata (Dict[str, Any]): The flat dictionary of metadata from an image.
213
+ propertysheet_map (Optional[Dict[int, Dict[str, Any]]]):
214
+ A map from a PropertySheet's `id()` to its configuration dict, which
215
+ should contain its `type` and a list of `prefixes`.
216
+ gradio_component_map (Optional[Dict[int, str]]):
217
+ An optional map from a standard component's `id()` to the exact
218
+ metadata key that should populate it.
219
+ remove_prefix_from_keys (bool): If True, enables fallback matching
220
+ by stripping prefixes from metadata keys for standard components.
221
+
222
+ Returns:
223
+ List[Any]: A list of `gr.update` or `gr.skip()` objects.
224
+ """
225
+ if propertysheet_map is None: propertysheet_map = {}
226
+ if gradio_component_map is None: gradio_component_map = {}
227
+
228
+ output_values = [None] * len(output_fields)
229
+ component_to_index = {id(comp): i for i, comp in enumerate(output_fields)}
230
+
231
+ # Pre-process metadata to create a map of base labels if needed.
232
+ base_label_map = {}
233
+ if remove_prefix_from_keys:
234
+ base_label_map = {key.rsplit(' - ', 1)[-1]: value for key, value in metadata.items()}
235
+
236
+ for component in output_fields:
237
+ comp_id = id(component)
238
+ output_index = component_to_index.get(comp_id)
239
+ if output_index is None:
240
+ continue
241
+
242
+ # --- Logic for PropertySheets ---
243
+ if comp_id in propertysheet_map:
244
+ sheet_info = propertysheet_map[comp_id]
245
+ dc_type = sheet_info.get("type")
246
+ prefix_definitions = sheet_info.get("prefixes", [])
247
+
248
+ # Build the list of actual prefix strings by interpreting the definitions.
249
+ prefix_values = []
250
+ for p_def in prefix_definitions:
251
+ # If the prefix definition is a key in the metadata, use its value.
252
+ if p_def in metadata:
253
+ prefix_values.append(str(metadata[p_def]))
254
+ # Otherwise, treat it as a literal string.
255
+ else:
256
+ prefix_values.append(p_def)
257
+
258
+ prefix_values = [p for p in prefix_values if p]
259
+
260
+ if not dc_type or not is_dataclass(dc_type):
261
+ continue
262
+
263
+ # Build the map from the dataclass structure to the expected full metadata keys.
264
+ path_to_key_map = build_path_to_metadata_key_map(dc_type, prefix_values)
265
+
266
+ # Get the base instance to start populating.
267
+ instance_to_populate = getattr(component, '_dataclass_value', dc_type())
268
+ if not is_dataclass(instance_to_populate):
269
+ instance_to_populate = dc_type()
270
+
271
+ # Populate the instance by iterating through the path map.
272
+ for path, metadata_key in path_to_key_map.items():
273
+ if metadata_key in metadata:
274
+ value_from_meta = metadata[metadata_key]
275
+
276
+ parts = path.split('.')
277
+ obj_to_set = instance_to_populate
278
+ try:
279
+ for part in parts[:-1]:
280
+ obj_to_set = getattr(obj_to_set, part)
281
+
282
+ final_field_name = parts[-1]
283
+ converted_value = infer_type(value_from_meta)
284
+ setattr(obj_to_set, final_field_name, converted_value)
285
+ except (AttributeError, KeyError, ValueError, TypeError) as e:
286
+ print(f"Warning (transfer_metadata): Could not set value for path '{path}'. Error: {e}")
287
+
288
+ output_values[output_index] = instance_to_populate
289
+
290
+ # --- Unified Logic for Standard Gradio Components ---
291
+ else:
292
+ value_to_set = None
293
+
294
+ # Priority 1: Check for an explicit mapping via gradio_component_map.
295
+ if comp_id in gradio_component_map:
296
+ metadata_key = gradio_component_map[comp_id]
297
+ if metadata_key in metadata:
298
+ value_to_set = metadata[metadata_key]
299
+
300
+ # If no explicit mapping was found, proceed to label-based matching.
301
+ if value_to_set is None:
302
+ label = getattr(component, 'label', None)
303
+ if label:
304
+ # Priority 2: Check for a base label match if the flag is set.
305
+ if remove_prefix_from_keys and label in base_label_map:
306
+ value_to_set = base_label_map[label]
307
+ # Priority 3: Fallback to an exact label match.
308
+ elif label in metadata:
309
+ value_to_set = metadata[label]
310
+
311
+ # If a value was found by any method, create the update object.
312
+ if value_to_set is not None:
313
+ output_values[output_index] = infer_type(value_to_set)
314
+
315
+ return output_values
src/backend/gradio_mediagallery/mediagallery.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """gr.Gallery() component."""
2
+
3
+ from __future__ import annotations
4
+ from collections.abc import Sequence
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from pathlib import Path
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Callable,
11
+ Literal,
12
+ Union,
13
+ )
14
+ from urllib.parse import urlparse
15
+ import PIL.Image
16
+ import numpy as np
17
+ from gradio_client import handle_file, utils as client_utils
18
+ from gradio_client.utils import is_http_url_like
19
+ from gradio import processing_utils, utils
20
+ from gradio.components.base import Component
21
+ from gradio.data_classes import FileData, GradioModel, GradioRootModel, ImageData
22
+ from gradio.events import EventListener, Events # Remova EventListener se não for usado
23
+ # Remova todas as importações e classes relacionadas à árvore (TreeNode, JsonData, etc.)
24
+
25
+ from gradio.i18n import I18nData
26
+
27
+ if TYPE_CHECKING:
28
+ from gradio.components import Timer
29
+ # --- DEFINIÇÕES DE MODELO SIMPLIFICADAS ---
30
+
31
+ class GalleryImage(GradioModel):
32
+ image: ImageData
33
+ caption: str | None = None
34
+
35
+ class GalleryVideo(GradioModel):
36
+ video: FileData
37
+ caption: str | None = None
38
+
39
+ class GalleryData(GradioRootModel): # Voltamos a usar GradioRootModel
40
+ root: list[Union[GalleryImage, GalleryVideo]]
41
+
42
+ class MediaGallery(Component):
43
+ """
44
+ Creates a gallery component that allows displaying a grid of images or videos, and optionally captions. If used as an input, the user can upload images or videos to the gallery.
45
+ If used as an output, the user can click on individual images or videos to view them at a higher resolution.
46
+
47
+ Demos: fake_gan, gif_maker
48
+ """
49
+
50
+ EVENTS = [
51
+ Events.select,
52
+ Events.change,
53
+ Events.delete,
54
+ EventListener(
55
+ "preview_close",
56
+ doc="This event is triggered when the MediaGallery preview is closed by the user",
57
+ ),
58
+ EventListener(
59
+ "preview_open",
60
+ doc="This event is triggered when the MediaGallery preview is opened by the user",
61
+ ),
62
+ EventListener(
63
+ "load_metadata",
64
+ doc="Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata.",
65
+ ),
66
+ ]
67
+
68
+ data_model = GalleryData
69
+
70
+ def __init__(
71
+ self,
72
+ value: (
73
+ Sequence[np.ndarray | PIL.Image.Image | str | Path | tuple]
74
+ | Callable
75
+ | None
76
+ ) = None,
77
+ *,
78
+ file_types: list[str] | None = None,
79
+ label: str | I18nData | None = None,
80
+ every: Timer | float | None = None,
81
+ inputs: Component | Sequence[Component] | set[Component] | None = None,
82
+ show_label: bool | None = None,
83
+ container: bool = True,
84
+ scale: int | None = None,
85
+ min_width: int = 160,
86
+ visible: bool | Literal["hidden"] = True,
87
+ elem_id: str | None = None,
88
+ elem_classes: list[str] | str | None = None,
89
+ render: bool = True,
90
+ key: int | str | tuple[int | str, ...] | None = None,
91
+ preserved_by_key: list[str] | str | None = "value",
92
+ columns: int | None = 2,
93
+ rows: int | None = None,
94
+ height: int | float | str | None = None,
95
+ allow_preview: bool = True,
96
+ preview: bool | None = None,
97
+ selected_index: int | None = None,
98
+ object_fit: (
99
+ Literal["contain", "cover", "fill", "none", "scale-down"] | None
100
+ ) = None,
101
+ show_share_button: bool | None = None,
102
+ show_download_button: bool | None = True,
103
+ interactive: bool | None = None,
104
+ type: Literal["numpy", "pil", "filepath"] = "filepath",
105
+ show_fullscreen_button: bool = True,
106
+ only_custom_metadata: bool = True,
107
+ popup_metadata_width: int | str = 500
108
+ ):
109
+ """
110
+ Parameters:
111
+ value: List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.
112
+ format: Format to save images before they are returned to the frontend, such as 'jpeg' or 'png'. This parameter only applies to images that are returned from the prediction function as numpy arrays or PIL Images. The format should be supported by the PIL library.
113
+ file_types: List of file extensions or types of files to be uploaded (e.g. ['image', '.mp4']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.
114
+ label: the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.
115
+ every: Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.
116
+ inputs: Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.
117
+ show_label: if True, will display label.
118
+ container: If True, will place the component in a container - providing some extra padding around the border.
119
+ scale: relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.
120
+ min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.
121
+ visible: If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM
122
+ elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
123
+ elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
124
+ render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
125
+ key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.
126
+ preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.
127
+ columns: Represents the number of images that should be shown in one row.
128
+ rows: Represents the number of rows in the image grid.
129
+ height: The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.
130
+ allow_preview: If True, images in the gallery will be enlarged when they are clicked. Default is True.
131
+ preview: If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.
132
+ selected_index: The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.
133
+ object_fit: CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".
134
+ show_share_button: If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.
135
+ show_download_button: If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.
136
+ interactive: If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.
137
+ type: The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.
138
+ show_fullscreen_button: If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.
139
+ only_custom_metadata: If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.
140
+ popup_metadata_width: The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").
141
+ """
142
+ self.columns = columns
143
+ self.rows = rows
144
+ self.height = height
145
+ self.preview = preview
146
+ self.object_fit = object_fit
147
+ self.allow_preview = allow_preview
148
+ self.show_download_button = (
149
+ (utils.get_space() is not None)
150
+ if show_download_button is None
151
+ else show_download_button
152
+ )
153
+ self.selected_index = selected_index
154
+ if type not in ["numpy", "pil", "filepath"]:
155
+ raise ValueError(
156
+ f"Invalid type: {type}. Must be one of ['numpy', 'pil', 'filepath']"
157
+ )
158
+ self.type = type
159
+ self.show_fullscreen_button = show_fullscreen_button
160
+ self.file_types = file_types
161
+
162
+ self.only_custom_metadata = only_custom_metadata
163
+ self.popup_metadata_width = popup_metadata_width
164
+
165
+ self.show_share_button = (
166
+ (utils.get_space() is not None)
167
+ if show_share_button is None
168
+ else show_share_button
169
+ )
170
+ super().__init__(
171
+ label=label,
172
+ every=every,
173
+ inputs=inputs,
174
+ show_label=show_label,
175
+ container=container,
176
+ scale=scale,
177
+ min_width=min_width,
178
+ visible=visible,
179
+ elem_id=elem_id,
180
+ elem_classes=elem_classes,
181
+ render=render,
182
+ key=key,
183
+ preserved_by_key=preserved_by_key,
184
+ value=value,
185
+ interactive=interactive,
186
+ )
187
+ self._value_description = f"a list of {'string filepaths' if type == 'filepath' else 'numpy arrays' if type == 'numpy' else 'PIL images'}"
188
+
189
+ def preprocess(self, payload: GalleryData | None) -> Any:
190
+ if payload is None:
191
+ return None
192
+ return [
193
+ (item.video.path if isinstance(item, GalleryVideo) else item.image.path, item.caption)
194
+ for item in payload.root
195
+ ] # Extrai os dados do wrapper
196
+
197
+ def postprocess(
198
+ self,
199
+ value: list | None,
200
+ ) -> GalleryData: # Assinatura está correta: retorna um dicionário
201
+
202
+ if value is None:
203
+ return GalleryData(root=[])
204
+
205
+ if isinstance(value, str):
206
+ raise ValueError(
207
+ "The `value` passed into `gr.Gallery` must be a list of images or videos, or list of (media, caption) tuples."
208
+ )
209
+
210
+ output_items = []
211
+
212
+ def _save_item(item):
213
+ img, caption = (item, None) if not isinstance(item, (tuple, list)) else item
214
+
215
+ # Lógica para extrair nome original (importante para nossa UI)
216
+ orig_name = None
217
+ if hasattr(img, 'filename') and img.filename:
218
+ orig_name = Path(img.filename).name
219
+
220
+ # Lógica de salvar no cache e preparar dados
221
+ if isinstance(img, np.ndarray):
222
+ file = processing_utils.save_img_array_to_cache(img, cache_dir=self.GRADIO_CACHE, format="png")
223
+ file_path = str(utils.abspath(file))
224
+ url = None
225
+ elif isinstance(img, PIL.Image.Image):
226
+ file = processing_utils.save_pil_to_cache(img, cache_dir=self.GRADIO_CACHE, format=str(img.format).lower())
227
+ file_path = str(utils.abspath(file))
228
+ url = None
229
+ elif isinstance(img, (str, Path)):
230
+ file_path = str(img)
231
+ if not orig_name:
232
+ orig_name = Path(file_path).name
233
+ url = file_path if is_http_url_like(file_path) else None
234
+ else:
235
+ raise ValueError(f"Cannot process type: {type(img)}")
236
+
237
+ mime_type = client_utils.get_mimetype(file_path)
238
+
239
+ if mime_type and "video" in mime_type:
240
+ file = processing_utils.save_file_to_cache(img, cache_dir=self.GRADIO_CACHE)
241
+ file_path = str(utils.abspath(file))
242
+ return GalleryVideo(
243
+ video=FileData(path=file_path, url=url, orig_name=orig_name, mime_type=mime_type),
244
+ caption=caption
245
+ )
246
+ else:
247
+ return GalleryImage(
248
+ image=ImageData(path=file_path, url=url, orig_name=orig_name, mime_type=mime_type),
249
+ caption=caption
250
+ )
251
+
252
+ with ThreadPoolExecutor() as executor:
253
+ for item in executor.map(_save_item, value):
254
+ if item:
255
+ output_items.append(item)
256
+
257
+ return GalleryData(root=output_items)
258
+
259
+ @staticmethod
260
+ def convert_to_type(img: str, type: Literal["filepath", "numpy", "pil"]):
261
+ if type == "filepath":
262
+ return img
263
+ else:
264
+ converted_image = PIL.Image.open(img)
265
+ if type == "numpy":
266
+ converted_image = np.array(converted_image)
267
+ return converted_image
268
+
269
+ def example_payload(self) -> Any:
270
+ return [
271
+ {
272
+ "image": handle_file(
273
+ "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"
274
+ )
275
+ },
276
+ ]
277
+
278
+ def example_value(self) -> Any:
279
+ return [
280
+ "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"
281
+ ]
src/backend/gradio_mediagallery/templates/component/assets/worker-BAOIWoxA.js ADDED
@@ -0,0 +1 @@
 
 
1
+ (function(){"use strict";const i="https://unpkg.com/@ffmpeg/core@0.12.9/dist/umd/ffmpeg-core.js";var E;(function(t){t.LOAD="LOAD",t.EXEC="EXEC",t.FFPROBE="FFPROBE",t.WRITE_FILE="WRITE_FILE",t.READ_FILE="READ_FILE",t.DELETE_FILE="DELETE_FILE",t.RENAME="RENAME",t.CREATE_DIR="CREATE_DIR",t.LIST_DIR="LIST_DIR",t.DELETE_DIR="DELETE_DIR",t.ERROR="ERROR",t.DOWNLOAD="DOWNLOAD",t.PROGRESS="PROGRESS",t.LOG="LOG",t.MOUNT="MOUNT",t.UNMOUNT="UNMOUNT"})(E||(E={}));const f=new Error("unknown message type"),a=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),u=new Error("failed to import ffmpeg-core.js");let r;const O=async({coreURL:t,wasmURL:n,workerURL:e})=>{const o=!r;try{t||(t=i),importScripts(t)}catch{if((!t||t===i)&&(t=i.replace("/umd/","/esm/")),self.createFFmpegCore=(await import(t)).default,!self.createFFmpegCore)throw u}const s=t,c=n||t.replace(/.js$/g,".wasm"),p=e||t.replace(/.js$/g,".worker.js");return r=await self.createFFmpegCore({mainScriptUrlOrBlob:`${s}#${btoa(JSON.stringify({wasmURL:c,workerURL:p}))}`}),r.setLogger(R=>self.postMessage({type:E.LOG,data:R})),r.setProgress(R=>self.postMessage({type:E.PROGRESS,data:R})),o},m=({args:t,timeout:n=-1})=>{r.setTimeout(n),r.exec(...t);const e=r.ret;return r.reset(),e},l=({args:t,timeout:n=-1})=>{r.setTimeout(n),r.ffprobe(...t);const e=r.ret;return r.reset(),e},D=({path:t,data:n})=>(r.FS.writeFile(t,n),!0),S=({path:t,encoding:n})=>r.FS.readFile(t,{encoding:n}),I=({path:t})=>(r.FS.unlink(t),!0),L=({oldPath:t,newPath:n})=>(r.FS.rename(t,n),!0),N=({path:t})=>(r.FS.mkdir(t),!0),A=({path:t})=>{const n=r.FS.readdir(t),e=[];for(const o of n){const s=r.FS.stat(`${t}/${o}`),c=r.FS.isDir(s.mode);e.push({name:o,isDir:c})}return e},k=({path:t})=>(r.FS.rmdir(t),!0),w=({fsType:t,options:n,mountPoint:e})=>{const o=t,s=r.FS.filesystems[o];return s?(r.FS.mount(s,n,e),!0):!1},b=({mountPoint:t})=>(r.FS.unmount(t),!0);self.onmessage=async({data:{id:t,type:n,data:e}})=>{const o=[];let s;try{if(n!==E.LOAD&&!r)throw a;switch(n){case E.LOAD:s=await O(e);break;case E.EXEC:s=m(e);break;case E.FFPROBE:s=l(e);break;case E.WRITE_FILE:s=D(e);break;case E.READ_FILE:s=S(e);break;case E.DELETE_FILE:s=I(e);break;case E.RENAME:s=L(e);break;case E.CREATE_DIR:s=N(e);break;case E.LIST_DIR:s=A(e);break;case E.DELETE_DIR:s=k(e);break;case E.MOUNT:s=w(e);break;case E.UNMOUNT:s=b(e);break;default:throw f}}catch(c){self.postMessage({id:t,type:E.ERROR,data:c.toString()});return}s instanceof Uint8Array&&o.push(s.buffer),self.postMessage({id:t,type:n,data:s},o)}})();
src/backend/gradio_mediagallery/templates/component/index.js ADDED
The diff for this file is too large to render. See raw diff
 
src/backend/gradio_mediagallery/templates/component/style.css ADDED
@@ -0,0 +1 @@
 
 
1
+ .block.svelte-239wnu{position:relative;margin:0;box-shadow:var(--block-shadow);border-width:var(--block-border-width);border-color:var(--block-border-color);border-radius:var(--block-radius);background:var(--block-background-fill);width:100%;line-height:var(--line-sm)}.block.fullscreen.svelte-239wnu{border-radius:0}.auto-margin.svelte-239wnu{margin-left:auto;margin-right:auto}.block.border_focus.svelte-239wnu{border-color:var(--color-accent)}.block.border_contrast.svelte-239wnu{border-color:var(--body-text-color)}.padded.svelte-239wnu{padding:var(--block-padding)}.hidden.svelte-239wnu{display:none}.flex.svelte-239wnu{display:flex;flex-direction:column}.hide-container.svelte-239wnu:not(.fullscreen){margin:0;box-shadow:none;--block-border-width:0;background:transparent;padding:0;overflow:visible}.resize-handle.svelte-239wnu{position:absolute;bottom:0;right:0;width:10px;height:10px;fill:var(--block-border-color);cursor:nwse-resize}.fullscreen.svelte-239wnu{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1000;overflow:auto}.animating.svelte-239wnu{animation:svelte-239wnu-pop-out .1s ease-out forwards}@keyframes svelte-239wnu-pop-out{0%{position:fixed;top:var(--start-top);left:var(--start-left);width:var(--start-width);height:var(--start-height);z-index:100}to{position:fixed;top:0vh;left:0vw;width:100vw;height:100vh;z-index:1000}}.placeholder.svelte-239wnu{border-radius:var(--block-radius);border-width:var(--block-border-width);border-color:var(--block-border-color);border-style:dashed}Tables */ table,tr,td,th{margin-top:var(--spacing-sm);margin-bottom:var(--spacing-sm);padding:var(--spacing-xl)}.md code,.md pre{background:none;font-family:var(--font-mono);font-size:var(--text-sm);text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:2;tab-size:2;-webkit-hyphens:none;hyphens:none}.md pre[class*=language-]::selection,.md pre[class*=language-] ::selection,.md code[class*=language-]::selection,.md code[class*=language-] ::selection{text-shadow:none;background:#b3d4fc}.md pre{padding:1em;margin:.5em 0;overflow:auto;position:relative;margin-top:var(--spacing-sm);margin-bottom:var(--spacing-sm);box-shadow:none;border:none;border-radius:var(--radius-md);background:var(--code-background-fill);padding:var(--spacing-xxl);font-family:var(--font-mono);text-shadow:none;border-radius:var(--radius-sm);white-space:nowrap;display:block;white-space:pre}.md :not(pre)>code{padding:.1em;border-radius:var(--radius-xs);white-space:normal;background:var(--code-background-fill);border:1px solid var(--panel-border-color);padding:var(--spacing-xxs) var(--spacing-xs)}.md .token.comment,.md .token.prolog,.md .token.doctype,.md .token.cdata{color:#708090}.md .token.punctuation{color:#999}.md .token.namespace{opacity:.7}.md .token.property,.md .token.tag,.md .token.boolean,.md .token.number,.md .token.constant,.md .token.symbol,.md .token.deleted{color:#905}.md .token.selector,.md .token.attr-name,.md .token.string,.md .token.char,.md .token.builtin,.md .token.inserted{color:#690}.md .token.atrule,.md .token.attr-value,.md .token.keyword{color:#07a}.md .token.function,.md .token.class-name{color:#dd4a68}.md .token.regex,.md .token.important,.md .token.variable{color:#e90}.md .token.important,.md .token.bold{font-weight:700}.md .token.italic{font-style:italic}.md .token.entity{cursor:help}.dark .md .token.comment,.dark .md .token.prolog,.dark .md .token.cdata{color:#5c6370}.dark .md .token.doctype,.dark .md .token.punctuation,.dark .md .token.entity{color:#abb2bf}.dark .md .token.attr-name,.dark .md .token.class-name,.dark .md .token.boolean,.dark .md .token.constant,.dark .md .token.number,.dark .md .token.atrule{color:#d19a66}.dark .md .token.keyword{color:#c678dd}.dark .md .token.property,.dark .md .token.tag,.dark .md .token.symbol,.dark .md .token.deleted,.dark .md .token.important{color:#e06c75}.dark .md .token.selector,.dark .md .token.string,.dark .md .token.char,.dark .md .token.builtin,.dark .md .token.inserted,.dark .md .token.regex,.dark .md .token.attr-value,.dark .md .token.attr-value>.token.punctuation{color:#98c379}.dark .md .token.variable,.dark .md .token.operator,.dark .md .token.function{color:#61afef}.dark .md .token.url{color:#56b6c2}span.svelte-1m32c2s div[class*=code_wrap]{position:relative}span.svelte-1m32c2s span.katex{font-size:var(--text-lg);direction:ltr}span.svelte-1m32c2s div[class*=code_wrap]>button{z-index:1;cursor:pointer;border-bottom-left-radius:var(--radius-sm);padding:var(--spacing-md);width:25px;height:25px;position:absolute;right:0}span.svelte-1m32c2s .check{opacity:0;z-index:var(--layer-top);transition:opacity .2s;background:var(--code-background-fill);color:var(--body-text-color);position:absolute;top:var(--size-1-5);left:var(--size-1-5)}span.svelte-1m32c2s p:not(:first-child){margin-top:var(--spacing-xxl)}span.svelte-1m32c2s .md-header-anchor{margin-left:-25px;padding-right:8px;line-height:1;color:var(--body-text-color-subdued);opacity:0}span.svelte-1m32c2s h1:hover .md-header-anchor,span.svelte-1m32c2s h2:hover .md-header-anchor,span.svelte-1m32c2s h3:hover .md-header-anchor,span.svelte-1m32c2s h4:hover .md-header-anchor,span.svelte-1m32c2s h5:hover .md-header-anchor,span.svelte-1m32c2s h6:hover .md-header-anchor{opacity:1}span.md.svelte-1m32c2s .md-header-anchor>svg{color:var(--body-text-color-subdued)}span.svelte-1m32c2s table{word-break:break-word}div.svelte-17qq50w>.md.prose{font-weight:var(--block-info-text-weight);font-size:var(--block-info-text-size);line-height:var(--line-sm)}div.svelte-17qq50w>.md.prose *{color:var(--block-info-text-color)}div.svelte-17qq50w{margin-bottom:var(--spacing-md)}span.has-info.svelte-zgrq3{margin-bottom:var(--spacing-xs)}span.svelte-zgrq3:not(.has-info){margin-bottom:var(--spacing-lg)}span.svelte-zgrq3{display:inline-block;position:relative;z-index:var(--layer-4);border:solid var(--block-title-border-width) var(--block-title-border-color);border-radius:var(--block-title-radius);background:var(--block-title-background-fill);padding:var(--block-title-padding);color:var(--block-title-text-color);font-weight:var(--block-title-text-weight);font-size:var(--block-title-text-size);line-height:var(--line-sm)}span[dir=rtl].svelte-zgrq3{display:block}.hide.svelte-zgrq3{margin:0;height:0}label.svelte-igqdol.svelte-igqdol{display:inline-flex;align-items:center;z-index:var(--layer-2);box-shadow:var(--block-label-shadow);border:var(--block-label-border-width) solid var(--block-label-border-color);border-top:none;border-left:none;border-radius:var(--block-label-radius);background:var(--block-label-background-fill);padding:var(--block-label-padding);pointer-events:none;color:var(--block-label-text-color);font-weight:var(--block-label-text-weight);font-size:var(--block-label-text-size);line-height:var(--line-sm)}.gr-group label.svelte-igqdol.svelte-igqdol{border-top-left-radius:0}label.float.svelte-igqdol.svelte-igqdol{position:absolute;top:var(--block-label-margin);left:var(--block-label-margin)}label.svelte-igqdol.svelte-igqdol:not(.float){position:static;margin-top:var(--block-label-margin);margin-left:var(--block-label-margin)}.hide.svelte-igqdol.svelte-igqdol{display:none}span.svelte-igqdol.svelte-igqdol{opacity:.8;margin-right:var(--size-2);width:calc(var(--block-label-text-size) - 1px);height:calc(var(--block-label-text-size) - 1px)}.hide-label.svelte-igqdol.svelte-igqdol{box-shadow:none;border-width:0;background:transparent;overflow:visible}label[dir=rtl].svelte-igqdol.svelte-igqdol{border:var(--block-label-border-width) solid var(--block-label-border-color);border-top:none;border-right:none;border-bottom-left-radius:var(--block-radius);border-bottom-right-radius:var(--block-label-radius);border-top-left-radius:var(--block-label-radius)}label[dir=rtl].svelte-igqdol span.svelte-igqdol{margin-left:var(--size-2);margin-right:0}.unstyled-link.svelte-151nsdd{all:unset;cursor:pointer}button.svelte-y0enk4{display:flex;justify-content:center;align-items:center;gap:1px;z-index:var(--layer-2);border-radius:var(--radius-xs);color:var(--block-label-text-color);border:1px solid var(--border-color);padding:var(--spacing-xxs)}button.svelte-y0enk4:hover{background-color:var(--background-fill-secondary)}button[disabled].svelte-y0enk4{opacity:.5;box-shadow:none}button[disabled].svelte-y0enk4:hover{cursor:not-allowed}.padded.svelte-y0enk4{background:var(--bg-color)}button.svelte-y0enk4:hover,button.highlight.svelte-y0enk4{cursor:pointer;color:var(--color-accent)}.padded.svelte-y0enk4:hover{color:var(--block-label-text-color)}span.svelte-y0enk4{padding:0 1px;font-size:10px}div.svelte-y0enk4{display:flex;align-items:center;justify-content:center;transition:filter .2s ease-in-out}.x-small.svelte-y0enk4{width:10px;height:10px}.small.svelte-y0enk4{width:14px;height:14px}.medium.svelte-y0enk4{width:20px;height:20px}.large.svelte-y0enk4{width:22px;height:22px}.pending.svelte-y0enk4{animation:svelte-y0enk4-flash .5s infinite}@keyframes svelte-y0enk4-flash{0%{opacity:.5}50%{opacity:1}to{opacity:.5}}.transparent.svelte-y0enk4{background:transparent;border:none;box-shadow:none}.empty.svelte-3w3rth{display:flex;justify-content:center;align-items:center;margin-top:calc(0px - var(--size-6));height:var(--size-full)}.icon.svelte-3w3rth{opacity:.5;height:var(--size-5);color:var(--body-text-color)}.small.svelte-3w3rth{min-height:calc(var(--size-32) - 20px)}.large.svelte-3w3rth{min-height:calc(var(--size-64) - 20px)}.unpadded_box.svelte-3w3rth{margin-top:0}.small_parent.svelte-3w3rth{min-height:100%!important}.dropdown-arrow.svelte-145leq6,.dropdown-arrow.svelte-ihhdbf{fill:currentColor}.circle.svelte-ihhdbf{fill:currentColor;opacity:.1}svg.svelte-pb9pol{animation:svelte-pb9pol-spin 1.5s linear infinite}@keyframes svelte-pb9pol-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}h2.svelte-1xg7h5n{font-size:var(--text-xl)!important}p.svelte-1xg7h5n,h2.svelte-1xg7h5n{white-space:pre-line}.wrap.svelte-1xg7h5n{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:var(--size-60);color:var(--block-label-text-color);line-height:var(--line-md);height:100%;padding-top:var(--size-3);text-align:center;margin:auto var(--spacing-lg)}.or.svelte-1xg7h5n{color:var(--body-text-color-subdued);display:flex}.icon-wrap.svelte-1xg7h5n{width:30px;margin-bottom:var(--spacing-lg)}@media (--screen-md){.wrap.svelte-1xg7h5n{font-size:var(--text-lg)}}.hovered.svelte-1xg7h5n{color:var(--color-accent)}div.svelte-q32hvf{border-top:1px solid transparent;display:flex;max-height:100%;justify-content:center;align-items:center;gap:var(--spacing-sm);height:auto;align-items:flex-end;color:var(--block-label-text-color);flex-shrink:0}.show_border.svelte-q32hvf{border-top:1px solid var(--block-border-color);margin-top:var(--spacing-xxl);box-shadow:var(--shadow-drop)}.source-selection.svelte-15ls1gu{display:flex;align-items:center;justify-content:center;border-top:1px solid var(--border-color-primary);width:100%;margin-left:auto;margin-right:auto;height:var(--size-10)}.icon.svelte-15ls1gu{width:22px;height:22px;margin:var(--spacing-lg) var(--spacing-xs);padding:var(--spacing-xs);color:var(--neutral-400);border-radius:var(--radius-md)}.selected.svelte-15ls1gu{color:var(--color-accent)}.icon.svelte-15ls1gu:hover,.icon.svelte-15ls1gu:focus{color:var(--color-accent)}.icon-button-wrapper.svelte-1h0hs6p{display:flex;flex-direction:row;align-items:center;justify-content:center;z-index:var(--layer-2);gap:var(--spacing-sm);box-shadow:var(--shadow-drop);border:1px solid var(--border-color-primary);background:var(--block-background-fill);padding:var(--spacing-xxs)}.icon-button-wrapper.hide-top-corner.svelte-1h0hs6p{border-top:none;border-right:none;border-radius:var(--block-label-right-radius)}.icon-button-wrapper.display-top-corner.svelte-1h0hs6p{border-radius:var(--radius-sm) 0 0 var(--radius-sm);top:var(--spacing-sm);right:-1px}.icon-button-wrapper.svelte-1h0hs6p:not(.top-panel){border:1px solid var(--border-color-primary);border-radius:var(--radius-sm)}.top-panel.svelte-1h0hs6p{position:absolute;top:var(--block-label-margin);right:var(--block-label-margin);margin:0}.icon-button-wrapper.svelte-1h0hs6p button{margin:var(--spacing-xxs);border-radius:var(--radius-xs);position:relative}.icon-button-wrapper.svelte-1h0hs6p a.download-link:not(:last-child),.icon-button-wrapper.svelte-1h0hs6p button:not(:last-child){margin-right:var(--spacing-xxs)}.icon-button-wrapper.svelte-1h0hs6p a.download-link:not(:last-child):not(.no-border *):after,.icon-button-wrapper.svelte-1h0hs6p button:not(:last-child):not(.no-border *):after{content:"";position:absolute;right:-4.5px;top:15%;height:70%;width:1px;background-color:var(--border-color-primary)}.icon-button-wrapper.svelte-1h0hs6p>*{height:100%}img.svelte-kxeri3{object-fit:cover}.image-container.svelte-x2tujq.svelte-x2tujq{height:100%;position:relative;min-width:var(--size-20)}.image-container.svelte-x2tujq button.svelte-x2tujq{width:var(--size-full);height:var(--size-full);border-radius:var(--radius-lg);display:flex;align-items:center;justify-content:center}.image-frame.svelte-x2tujq img{width:var(--size-full);height:var(--size-full);object-fit:scale-down}.selectable.svelte-x2tujq.svelte-x2tujq{cursor:crosshair}.fullscreen-controls svg{position:relative;top:0}.image-container:fullscreen{background-color:#000;display:flex;justify-content:center;align-items:center}.image-container:fullscreen img{max-width:90vw;max-height:90vh;object-fit:scale-down}.image-frame.svelte-x2tujq.svelte-x2tujq{width:auto;height:100%;display:flex;align-items:center;justify-content:center}.overlay.svelte-1pwzuub{position:absolute;background-color:#0006;width:100%;height:100%}.hidden.svelte-1pwzuub{display:none}.load-wrap.svelte-1pwzuub{display:flex;justify-content:center;align-items:center;height:100%}.loader.svelte-1pwzuub{display:flex;position:relative;background-color:var(--border-color-accent-subdued);animation:svelte-1pwzuub-shadowPulse 2s linear infinite;box-shadow:-24px 0 var(--border-color-accent-subdued),24px 0 var(--border-color-accent-subdued);margin:var(--spacing-md);border-radius:50%;width:10px;height:10px;scale:.5}@keyframes svelte-1pwzuub-shadowPulse{33%{box-shadow:-24px 0 var(--border-color-accent-subdued),24px 0 #fff;background:#fff}66%{box-shadow:-24px 0 #fff,24px 0 #fff;background:var(--border-color-accent-subdued)}to{box-shadow:-24px 0 #fff,24px 0 var(--border-color-accent-subdued);background:#fff}}.gallery-container.svelte-1crbckc.svelte-1crbckc{position:relative;width:100%;height:100%}button.svelte-1crbckc.svelte-1crbckc{width:var(--size-full);height:var(--size-full);display:block;border-radius:var(--radius-lg)}.preview.svelte-1crbckc.svelte-1crbckc{display:flex;position:absolute;flex-direction:column;z-index:var(--layer-2);border-radius:calc(var(--block-radius) - var(--block-border-width));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);width:var(--size-full);height:var(--size-full)}.preview.svelte-1crbckc.svelte-1crbckc:focus-visible{outline:none}.preview.minimal.svelte-1crbckc.svelte-1crbckc{width:fit-content;height:fit-content}.preview.svelte-1crbckc.svelte-1crbckc:before{content:"";position:absolute;z-index:var(--layer-below);background:var(--background-fill-primary);opacity:.9;width:var(--size-full);height:var(--size-full)}.fixed-height.svelte-1crbckc.svelte-1crbckc{min-height:var(--size-80);max-height:65vh}@media (--screen-xl){.fixed-height.svelte-1crbckc.svelte-1crbckc{min-height:450px}}.media-container.svelte-1crbckc.svelte-1crbckc{height:calc(100% - var(--size-14));width:100%;display:flex;align-items:center;justify-content:center;cursor:pointer}.media-container.svelte-1crbckc img,.media-container.svelte-1crbckc video{max-width:100%;max-height:100%;object-fit:contain}.thumbnails.svelte-1crbckc img{object-fit:cover;width:var(--size-full);height:var(--size-full)}.thumbnails.svelte-1crbckc svg{position:absolute;top:var(--size-2);left:var(--size-2);width:50%;height:50%;opacity:50%}.caption.svelte-1crbckc.svelte-1crbckc{padding:var(--size-2) var(--size-3);overflow:hidden;color:var(--block-label-text-color);font-weight:var(--weight-semibold);text-align:center;text-overflow:ellipsis;white-space:nowrap;align-self:center}.thumbnails.svelte-1crbckc.svelte-1crbckc{display:flex;position:absolute;bottom:0;justify-content:flex-start;align-items:center;gap:var(--spacing-lg);width:var(--size-full);height:var(--size-14);overflow-x:scroll}.thumbnail-item.svelte-1crbckc.svelte-1crbckc{--ring-color:transparent;position:relative;box-shadow:inset 0 0 0 1px var(--ring-color),var(--shadow-drop);border:1px solid var(--border-color-primary);border-radius:var(--button-small-radius);background:var(--background-fill-secondary);aspect-ratio:var(--ratio-square);width:var(--size-full);height:var(--size-full);overflow:clip}.thumbnail-item.svelte-1crbckc.svelte-1crbckc:hover{--ring-color:var(--color-accent);border-color:var(--color-accent);filter:brightness(1.1)}.thumbnail-item.selected.svelte-1crbckc.svelte-1crbckc{--ring-color:var(--color-accent);border-color:var(--color-accent)}.thumbnail-item.svelte-1crbckc svg{position:absolute;top:50%;left:50%;width:50%;height:50%;opacity:50%;transform:translate(-50%,-50%)}.thumbnail-item.svelte-1crbckc video{width:var(--size-full);height:var(--size-full);overflow:hidden;object-fit:cover}.thumbnail-small.svelte-1crbckc.svelte-1crbckc{flex:none;transform:scale(.9);transition:75ms;width:var(--size-9);height:var(--size-9)}.thumbnail-small.selected.svelte-1crbckc.svelte-1crbckc{--ring-color:var(--color-accent);transform:scale(1);border-color:var(--color-accent)}.grid-wrap.svelte-1crbckc.svelte-1crbckc{position:relative;padding:var(--size-2);overflow-y:auto}.grid-wrap.fixed-height.svelte-1crbckc.svelte-1crbckc{min-height:var(--size-80);max-height:65vh}.grid-container.svelte-1crbckc.svelte-1crbckc{display:grid;position:relative;grid-template-rows:repeat(var(--grid-rows),minmax(100px,1fr));grid-template-columns:repeat(var(--grid-cols),minmax(100px,1fr));grid-auto-rows:minmax(100px,1fr);gap:var(--spacing-lg)}.single-item-wrapper.svelte-1crbckc.svelte-1crbckc{display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:var(--spacing-xxl);box-sizing:border-box}.single-item-wrapper.svelte-1crbckc .gallery-item-with-name.svelte-1crbckc{width:100%;height:100%;max-width:min(300px,80vw);max-height:min(320px,calc(80vh - var(--size-4)));display:flex;flex-direction:column;align-items:center}.single-item-wrapper.svelte-1crbckc .gallery-item.svelte-1crbckc{width:100%;height:100%;max-width:100%;max-height:100%}.single-item-wrapper.svelte-1crbckc .thumbnail-item.thumbnail-lg.svelte-1crbckc{display:flex!important;align-items:center!important;justify-content:center!important}.single-item-wrapper.svelte-1crbckc .thumbnail-filename.svelte-1crbckc{height:var(--size-4);line-height:var(--size-4)}.single-item-wrapper.svelte-1crbckc .thumbnail-lg.svelte-1crbckc>img,.single-item-wrapper.svelte-1crbckc .thumbnail-lg.svelte-1crbckc>video{object-fit:var(--object-fit)!important}.thumbnail-lg.svelte-1crbckc>img,.thumbnail-lg.svelte-1crbckc>video{width:var(--size-full);height:var(--size-full);overflow:hidden;object-fit:var(--object-fit)}.thumbnail-lg.svelte-1crbckc:hover .caption-label.svelte-1crbckc{opacity:.5}.caption-label.svelte-1crbckc.svelte-1crbckc{position:absolute;right:var(--block-label-margin);bottom:var(--block-label-margin);z-index:var(--layer-1);border-top:1px solid var(--border-color-primary);border-left:1px solid var(--border-color-primary);border-radius:var(--block-label-radius);background:var(--background-fill-secondary);padding:var(--block-label-padding);max-width:80%;overflow:hidden;font-size:var(--block-label-text-size);text-align:left;text-overflow:ellipsis;white-space:nowrap}.grid-wrap.minimal.svelte-1crbckc.svelte-1crbckc{padding:0}.gallery-item-with-name.svelte-1crbckc.svelte-1crbckc{display:flex;flex-direction:column;gap:var(--size-1);width:100%;height:100%}.thumbnail-filename.svelte-1crbckc.svelte-1crbckc{font-size:var(--text-xs);color:var(--body-text-color);text-align:center;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 var(--size-1)}.gallery-item.svelte-1crbckc.svelte-1crbckc{position:relative;width:100%;height:100%}.metadata-popup.svelte-1crbckc.svelte-1crbckc{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--background-fill-primary, white);border:1px solid var(--border-color-primary);box-shadow:0 4px 8px #0003;z-index:1000;border-radius:8px;max-width:min(90%,600px);max-height:min(50vh,calc(100% - 2rem));min-height:200px;display:flex;flex-direction:column;pointer-events:auto}.popup-content.svelte-1crbckc.svelte-1crbckc{padding:1rem;display:flex;flex-direction:column;width:100%;box-sizing:border-box;overflow-y:auto;position:relative}.close-button.svelte-1crbckc.svelte-1crbckc{position:absolute;top:.5rem;right:.5rem;background:none;border:none;font-size:1.25rem;cursor:pointer;z-index:20;color:var(--body-text-color);padding:.25rem;line-height:1;width:24px;height:24px;text-align:center}.popup-title.svelte-1crbckc.svelte-1crbckc{font-weight:700;margin:0 0 1rem;flex-shrink:0;padding-right:2.5rem}.metadata-table-container.svelte-1crbckc.svelte-1crbckc{flex-grow:1;overflow-y:auto;max-height:calc(100% - 5rem);min-height:0;margin-bottom:1rem}.metadata-table.svelte-1crbckc.svelte-1crbckc{width:100%;border-collapse:collapse;table-layout:auto}.metadata-label.svelte-1crbckc.svelte-1crbckc{background:var(--background-fill-secondary, #f5f5f5);padding:.5rem;font-weight:700;text-align:left;vertical-align:top;width:35%}.metadata-value.svelte-1crbckc.svelte-1crbckc{text-align:left;padding:.5rem;white-space:pre-wrap;word-break:break-all;vertical-align:top}.load-metadata-button.svelte-1crbckc.svelte-1crbckc{margin-top:1rem;padding:.5rem 1rem;background-color:var(--button-primary-background-fill);color:var(--button-primary-text-color);border:none;border-radius:4px;cursor:pointer;align-self:center;flex-shrink:0}.load-metadata-button.svelte-1crbckc.svelte-1crbckc:hover{background-color:var(--button-primary-background-fill-hover)}.no-metadata-message.svelte-1crbckc.svelte-1crbckc{flex-grow:1;display:flex;align-items:center;justify-content:center;color:var(--body-text-color-subdued)}svg.svelte-43sxxs.svelte-43sxxs{width:var(--size-20);height:var(--size-20)}svg.svelte-43sxxs path.svelte-43sxxs{fill:var(--loader-color)}div.svelte-43sxxs.svelte-43sxxs{z-index:var(--layer-2)}.margin.svelte-43sxxs.svelte-43sxxs{margin:var(--size-4)}.wrap.svelte-vusapu.svelte-vusapu{display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:var(--layer-3);transition:opacity .1s ease-in-out;border-radius:var(--block-radius);background:var(--block-background-fill);padding:0 var(--size-6);overflow:hidden;pointer-events:none}.wrap.center.svelte-vusapu.svelte-vusapu{top:0;right:0;left:0}.wrap.default.svelte-vusapu.svelte-vusapu{top:0;right:0;bottom:0;left:0}.hide.svelte-vusapu.svelte-vusapu{opacity:0;pointer-events:none}.generating.svelte-vusapu.svelte-vusapu{animation:svelte-vusapu-pulseStart 1s cubic-bezier(.4,0,.6,1),svelte-vusapu-pulse 2s cubic-bezier(.4,0,.6,1) 1s infinite;border:2px solid var(--color-accent);background:transparent;z-index:var(--layer-1);pointer-events:none}.translucent.svelte-vusapu.svelte-vusapu{background:none}@keyframes svelte-vusapu-pulseStart{0%{opacity:0}to{opacity:1}}@keyframes svelte-vusapu-pulse{0%,to{opacity:1}50%{opacity:.5}}.loading.svelte-vusapu.svelte-vusapu{z-index:var(--layer-2);color:var(--body-text-color)}.eta-bar.svelte-vusapu.svelte-vusapu{position:absolute;top:0;right:0;bottom:0;left:0;transform-origin:left;opacity:.8;z-index:var(--layer-1);transition:10ms;background:var(--background-fill-secondary)}.progress-bar-wrap.svelte-vusapu.svelte-vusapu{border:1px solid var(--border-color-primary);background:var(--background-fill-primary);width:55.5%;height:var(--size-4)}.progress-bar.svelte-vusapu.svelte-vusapu{transform-origin:left;background-color:var(--loader-color);width:var(--size-full);height:var(--size-full)}.progress-level.svelte-vusapu.svelte-vusapu{display:flex;flex-direction:column;align-items:center;gap:1;z-index:var(--layer-2);width:var(--size-full)}.progress-level-inner.svelte-vusapu.svelte-vusapu{margin:var(--size-2) auto;color:var(--body-text-color);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text.svelte-vusapu.svelte-vusapu{position:absolute;bottom:0;right:0;z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text-center.svelte-vusapu.svelte-vusapu{display:flex;position:absolute;top:0;right:0;justify-content:center;align-items:center;transform:translateY(var(--size-6));z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono);text-align:center}.error.svelte-vusapu.svelte-vusapu{box-shadow:var(--shadow-drop);border:solid 1px var(--error-border-color);border-radius:var(--radius-full);background:var(--error-background-fill);padding-right:var(--size-4);padding-left:var(--size-4);color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font)}.validation-error.svelte-vusapu.svelte-vusapu{pointer-events:auto;color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font);position:absolute;background:var(--error-background-fill);top:0;right:0;z-index:var(--layer-3);padding:var(--size-1) var(--size-2);font-size:var(--text-md);text-align:center;border-bottom-left-radius:var(--radius-sm);border-bottom:1px solid var(--error-border-color);border-left:1px solid var(--error-border-color);display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-xl)}.minimal.svelte-vusapu.svelte-vusapu{pointer-events:none}.minimal.svelte-vusapu .progress-text.svelte-vusapu{background:var(--block-background-fill)}.border.svelte-vusapu.svelte-vusapu{border:1px solid var(--border-color-primary)}.clear-status.svelte-vusapu.svelte-vusapu{position:absolute;display:flex;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-1)}.toast-body.svelte-syezpc{display:flex;position:relative;right:0;left:0;align-items:center;margin:var(--size-6) var(--size-4);margin:auto;border-radius:var(--container-radius);overflow:hidden;pointer-events:auto}.toast-body.error.svelte-syezpc{border:1px solid var(--color-red-700);background:var(--color-red-50)}.dark .toast-body.error.svelte-syezpc{border:1px solid var(--color-red-500);background-color:var(--color-grey-950)}.toast-body.warning.svelte-syezpc{border:1px solid var(--color-yellow-700);background:var(--color-yellow-50)}.dark .toast-body.warning.svelte-syezpc{border:1px solid var(--color-yellow-500);background-color:var(--color-grey-950)}.toast-body.info.svelte-syezpc{border:1px solid var(--color-grey-700);background:var(--color-grey-50)}.dark .toast-body.info.svelte-syezpc{border:1px solid var(--color-grey-500);background-color:var(--color-grey-950)}.toast-body.success.svelte-syezpc{border:1px solid var(--color-green-700);background:var(--color-green-50)}.dark .toast-body.success.svelte-syezpc{border:1px solid var(--color-green-500);background-color:var(--color-grey-950)}.toast-title.svelte-syezpc{display:flex;align-items:center;font-weight:var(--weight-bold);font-size:var(--text-lg);line-height:var(--line-sm)}.toast-title.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-title.error.svelte-syezpc{color:var(--color-red-50)}.toast-title.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-title.warning.svelte-syezpc{color:var(--color-yellow-50)}.toast-title.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-title.info.svelte-syezpc{color:var(--color-grey-50)}.toast-title.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-title.success.svelte-syezpc{color:var(--color-green-50)}.toast-close.svelte-syezpc{margin:0 var(--size-3);border-radius:var(--size-3);padding:0px var(--size-1-5);font-size:var(--size-5);line-height:var(--size-5)}.toast-close.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-close.error.svelte-syezpc{color:var(--color-red-500)}.toast-close.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-close.warning.svelte-syezpc{color:var(--color-yellow-500)}.toast-close.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-close.info.svelte-syezpc{color:var(--color-grey-500)}.toast-close.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-close.success.svelte-syezpc{color:var(--color-green-500)}.toast-text.svelte-syezpc{font-size:var(--text-lg);word-wrap:break-word;overflow-wrap:break-word;word-break:break-word}.toast-text.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-text.error.svelte-syezpc{color:var(--color-red-50)}.toast-text.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-text.warning.svelte-syezpc{color:var(--color-yellow-50)}.toast-text.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-text.info.svelte-syezpc{color:var(--color-grey-50)}.toast-text.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-text.success.svelte-syezpc{color:var(--color-green-50)}.toast-details.svelte-syezpc{margin:var(--size-3) var(--size-3) var(--size-3) 0;width:100%}.toast-icon.svelte-syezpc{display:flex;position:absolute;position:relative;flex-shrink:0;justify-content:center;align-items:center;margin:var(--size-2);border-radius:var(--radius-full);padding:var(--size-1);padding-left:calc(var(--size-1) - 1px);width:35px;height:35px}.toast-icon.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-icon.error.svelte-syezpc{color:var(--color-red-500)}.toast-icon.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-icon.warning.svelte-syezpc{color:var(--color-yellow-500)}.toast-icon.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-icon.info.svelte-syezpc{color:var(--color-grey-500)}.toast-icon.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-icon.success.svelte-syezpc{color:var(--color-green-500)}@keyframes svelte-syezpc-countdown{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.timer.svelte-syezpc{position:absolute;bottom:0;left:0;transform-origin:0 0;animation:svelte-syezpc-countdown 10s linear forwards;width:100%;height:var(--size-1)}.timer.error.svelte-syezpc{background:var(--color-red-700)}.dark .timer.error.svelte-syezpc{background:var(--color-red-500)}.timer.warning.svelte-syezpc{background:var(--color-yellow-700)}.dark .timer.warning.svelte-syezpc{background:var(--color-yellow-500)}.timer.info.svelte-syezpc{background:var(--color-grey-700)}.dark .timer.info.svelte-syezpc{background:var(--color-grey-500)}.timer.success.svelte-syezpc{background:var(--color-green-700)}.dark .timer.success.svelte-syezpc{background:var(--color-green-500)}.hidden.svelte-syezpc{display:none}.toast-text.svelte-syezpc a{text-decoration:underline}.toast-wrap.svelte-je2isz{--toast-top:var(--size-4);display:flex;position:fixed;top:calc(var(--toast-top) + var(--size-4));right:var(--size-4);flex-direction:column;align-items:end;gap:var(--size-2);z-index:var(--layer-top);width:calc(100% - var(--size-8))}@media (--screen-sm){.toast-wrap.svelte-je2isz{width:calc(var(--size-96) + var(--size-10))}}.streaming-bar.svelte-ga0jj6{position:absolute;bottom:0;left:0;right:0;height:4px;background-color:var(--primary-600);animation:svelte-ga0jj6-countdown linear forwards;z-index:1}@keyframes svelte-ga0jj6-countdown{0%{transform:translate(0)}to{transform:translate(-100%)}}:global(*::-webkit-scrollbar){width:8px;height:8px;background-color:transparent}:global(*::-webkit-scrollbar-track){background:transparent;border-radius:10px}:global(*::-webkit-scrollbar-thumb){background-color:#8886;border-radius:10px;border:2px solid transparent;background-clip:content-box}:global(*::-webkit-scrollbar-thumb:hover){background-color:#888888b3}:global(html){scrollbar-width:thin;scrollbar-color:rgba(136,136,136,.7) transparent}.gallery-container{position:relative;width:100%;height:100%}button{width:var(--size-full);height:var(--size-full);display:block;border-radius:var(--radius-lg)}.preview{display:flex;position:absolute;flex-direction:column;z-index:var(--layer-2);border-radius:calc(var(--block-radius) - var(--block-border-width));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);width:var(--size-full);height:var(--size-full)}.preview:focus-visible{outline:none}.preview.minimal{width:fit-content;height:fit-content}.preview:before{content:"";position:absolute;z-index:var(--layer-below);background:var(--background-fill-primary);opacity:.9;width:var(--size-full);height:var(--size-full)}.fixed-height{min-height:var(--size-80);max-height:65vh}@media (--screen-xl){.fixed-height{min-height:450px}}.media-container{height:calc(100% - var(--size-14));width:100%;display:flex;align-items:center;justify-content:center;cursor:pointer}.media-container :global(img),.media-container :global(video){max-width:100%;max-height:100%;object-fit:contain}.thumbnails :global(img){object-fit:cover;width:var(--size-full);height:var(--size-full)}.thumbnails :global(svg){position:absolute;top:var(--size-2);left:var(--size-2);width:50%;height:50%;opacity:50%}.caption{padding:var(--size-2) var(--size-3);overflow:hidden;color:var(--block-label-text-color);font-weight:var(--weight-semibold);text-align:center;text-overflow:ellipsis;white-space:nowrap;align-self:center}.thumbnails{display:flex;position:absolute;bottom:0;justify-content:flex-start;align-items:center;gap:var(--spacing-lg);width:var(--size-full);height:var(--size-14);overflow-x:scroll}.thumbnail-item{--ring-color: transparent;position:relative;box-shadow:inset 0 0 0 1px var(--ring-color),var(--shadow-drop);border:1px solid var(--border-color-primary);border-radius:var(--button-small-radius);background:var(--background-fill-secondary);aspect-ratio:var(--ratio-square);width:var(--size-full);height:var(--size-full);overflow:clip}.thumbnail-item:hover{--ring-color: var(--color-accent);border-color:var(--color-accent);filter:brightness(1.1)}.thumbnail-item.selected{--ring-color: var(--color-accent);border-color:var(--color-accent)}.thumbnail-item :global(svg){position:absolute;top:50%;left:50%;width:50%;height:50%;opacity:50%;transform:translate(-50%,-50%)}.thumbnail-item :global(video){width:var(--size-full);height:var(--size-full);overflow:hidden;object-fit:cover}.thumbnail-small{flex:none;transform:scale(.9);transition:75ms;width:var(--size-9);height:var(--size-9)}.thumbnail-small.selected{--ring-color: var(--color-accent);transform:scale(1);border-color:var(--color-accent)}.grid-wrap{position:relative;padding:var(--size-2);overflow-y:auto}.grid-wrap.fixed-height{min-height:var(--size-80);max-height:65vh}.grid-container{display:grid;position:relative;grid-template-rows:repeat(var(--grid-rows),minmax(100px,1fr));grid-template-columns:repeat(var(--grid-cols),minmax(100px,1fr));grid-auto-rows:minmax(100px,1fr);gap:var(--spacing-lg)}.single-item-wrapper{display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:var(--spacing-xxl);box-sizing:border-box}.single-item-wrapper .gallery-item-with-name{width:100%;height:100%;max-width:min(300px,80vw);max-height:min(320px,calc(80vh - var(--size-4)));display:flex;flex-direction:column;align-items:center}.single-item-wrapper .gallery-item{width:100%;height:100%;max-width:100%;max-height:100%}.single-item-wrapper .thumbnail-item.thumbnail-lg{display:flex!important;align-items:center!important;justify-content:center!important}.single-item-wrapper .thumbnail-filename{height:var(--size-4);line-height:var(--size-4)}.single-item-wrapper .thumbnail-lg>:global(img),.single-item-wrapper .thumbnail-lg>:global(video){object-fit:var(--object-fit)!important}.thumbnail-lg>:global(img),.thumbnail-lg>:global(video){width:var(--size-full);height:var(--size-full);overflow:hidden;object-fit:var(--object-fit)}.thumbnail-lg:hover .caption-label{opacity:.5}.caption-label{position:absolute;right:var(--block-label-margin);bottom:var(--block-label-margin);z-index:var(--layer-1);border-top:1px solid var(--border-color-primary);border-left:1px solid var(--border-color-primary);border-radius:var(--block-label-radius);background:var(--background-fill-secondary);padding:var(--block-label-padding);max-width:80%;overflow:hidden;font-size:var(--block-label-text-size);text-align:left;text-overflow:ellipsis;white-space:nowrap}.grid-wrap.minimal{padding:0}.gallery-item-with-name{display:flex;flex-direction:column;gap:var(--size-1);width:100%;height:100%}.thumbnail-filename{font-size:var(--text-xs);color:var(--body-text-color);text-align:center;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 var(--size-1)}.gallery-item{position:relative;width:100%;height:100%}.metadata-popup{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--background-fill-primary, white);border:1px solid var(--border-color-primary);box-shadow:0 4px 8px #0003;z-index:1000;border-radius:8px;max-width:min(90%,600px);max-height:min(50vh,calc(100% - 2rem));min-height:200px;display:flex;flex-direction:column;pointer-events:auto}.popup-content{padding:1rem;display:flex;flex-direction:column;width:100%;box-sizing:border-box;overflow-y:auto;position:relative}.close-button{position:absolute;top:.5rem;right:.5rem;background:none;border:none;font-size:1.25rem;cursor:pointer;z-index:20;color:var(--body-text-color);padding:.25rem;line-height:1;width:24px;height:24px;text-align:center}.popup-title{font-weight:700;margin:0 0 1rem;flex-shrink:0;padding-right:2.5rem}.metadata-table-container{flex-grow:1;overflow-y:auto;max-height:calc(100% - 5rem);min-height:0;margin-bottom:1rem}.metadata-table{width:100%;border-collapse:collapse;table-layout:auto}.metadata-label{background:var(--background-fill-secondary, #f5f5f5);padding:.5rem;font-weight:700;text-align:left;vertical-align:top;width:35%}.metadata-value{text-align:left;padding:.5rem;white-space:pre-wrap;word-break:break-all;vertical-align:top}.load-metadata-button{margin-top:1rem;padding:.5rem 1rem;background-color:var(--button-primary-background-fill);color:var(--button-primary-text-color);border:none;border-radius:4px;cursor:pointer;align-self:center;flex-shrink:0}.load-metadata-button:hover{background-color:var(--button-primary-background-fill-hover)}.no-metadata-message{flex-grow:1;display:flex;align-items:center;justify-content:center;color:var(--body-text-color-subdued)}.container.svelte-1onbytl.svelte-1onbytl{border-radius:var(--radius-lg);overflow:hidden}.container.selected.svelte-1onbytl.svelte-1onbytl{border:2px solid var(--border-color-accent)}.images-wrapper.svelte-1onbytl.svelte-1onbytl{display:flex;gap:var(--spacing-sm)}.container.table.svelte-1onbytl .images-wrapper.svelte-1onbytl{flex-direction:row;align-items:center;padding:var(--spacing-sm);border:1px solid var(--border-color-primary);border-radius:var(--radius-lg);background:var(--background-fill-secondary)}.container.gallery.svelte-1onbytl .images-wrapper.svelte-1onbytl{flex-direction:row;gap:0}.image-container.svelte-1onbytl.svelte-1onbytl{position:relative;flex-shrink:0}.container.table.svelte-1onbytl .image-container.svelte-1onbytl{width:var(--size-12);height:var(--size-12)}.container.gallery.svelte-1onbytl .image-container.svelte-1onbytl{width:var(--size-20);height:var(--size-20);margin-left:calc(-1 * var(--size-8))}.container.gallery.svelte-1onbytl .image-container.svelte-1onbytl:first-child{margin-left:0}.more-indicator.svelte-1onbytl.svelte-1onbytl{display:flex;align-items:center;justify-content:center;font-size:var(--text-lg);font-weight:700;color:var(--border-color-primary)}.container.table.svelte-1onbytl .more-indicator.svelte-1onbytl{width:var(--size-12);height:var(--size-12)}.container.gallery.svelte-1onbytl .more-indicator.svelte-1onbytl{width:var(--size-20);height:var(--size-20);margin-left:calc(-1 * var(--size-8));margin-right:calc(-1 * var(--size-6))}.image-container.svelte-1onbytl img.svelte-1onbytl,.image-container.svelte-1onbytl video.svelte-1onbytl{width:100%;height:100%;object-fit:cover;border-radius:var(--radius-md)}.caption.svelte-1onbytl.svelte-1onbytl{position:absolute;bottom:0;left:0;right:0;background:#000000b3;color:#fff;padding:var(--spacing-xs);font-size:var(--text-xs);text-align:center;border-radius:0 0 var(--radius-md) var(--radius-md);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.container.table.svelte-1onbytl .caption.svelte-1onbytl{display:none}
src/backend/gradio_mediagallery/templates/example/index.js ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const {
2
+ SvelteComponent: j,
3
+ append_hydration: d,
4
+ attr: f,
5
+ children: v,
6
+ claim_element: u,
7
+ claim_space: g,
8
+ claim_text: q,
9
+ destroy_each: G,
10
+ detach: o,
11
+ element: _,
12
+ empty: k,
13
+ ensure_array_like: E,
14
+ get_svelte_dataset: M,
15
+ init: O,
16
+ insert_hydration: h,
17
+ noop: I,
18
+ safe_not_equal: z,
19
+ set_data: A,
20
+ space: b,
21
+ src_url_equal: p,
22
+ text: P,
23
+ toggle_class: m
24
+ } = window.__gradio__svelte__internal;
25
+ function D(a, e, i) {
26
+ const s = a.slice();
27
+ return s[3] = e[i], s;
28
+ }
29
+ function V(a) {
30
+ let e, i, s = E(
31
+ /*value*/
32
+ a[0].slice(0, 3)
33
+ ), l = [];
34
+ for (let n = 0; n < s.length; n += 1)
35
+ l[n] = N(D(a, s, n));
36
+ let t = (
37
+ /*value*/
38
+ a[0].length > 3 && S()
39
+ );
40
+ return {
41
+ c() {
42
+ e = _("div");
43
+ for (let n = 0; n < l.length; n += 1)
44
+ l[n].c();
45
+ i = b(), t && t.c(), this.h();
46
+ },
47
+ l(n) {
48
+ e = u(n, "DIV", { class: !0 });
49
+ var r = v(e);
50
+ for (let c = 0; c < l.length; c += 1)
51
+ l[c].l(r);
52
+ i = g(r), t && t.l(r), r.forEach(o), this.h();
53
+ },
54
+ h() {
55
+ f(e, "class", "images-wrapper svelte-1onbytl");
56
+ },
57
+ m(n, r) {
58
+ h(n, e, r);
59
+ for (let c = 0; c < l.length; c += 1)
60
+ l[c] && l[c].m(e, null);
61
+ d(e, i), t && t.m(e, null);
62
+ },
63
+ p(n, r) {
64
+ if (r & /*value*/
65
+ 1) {
66
+ s = E(
67
+ /*value*/
68
+ n[0].slice(0, 3)
69
+ );
70
+ let c;
71
+ for (c = 0; c < s.length; c += 1) {
72
+ const y = D(n, s, c);
73
+ l[c] ? l[c].p(y, r) : (l[c] = N(y), l[c].c(), l[c].m(e, i));
74
+ }
75
+ for (; c < l.length; c += 1)
76
+ l[c].d(1);
77
+ l.length = s.length;
78
+ }
79
+ /*value*/
80
+ n[0].length > 3 ? t || (t = S(), t.c(), t.m(e, null)) : t && (t.d(1), t = null);
81
+ },
82
+ d(n) {
83
+ n && o(e), G(l, n), t && t.d();
84
+ }
85
+ };
86
+ }
87
+ function B(a) {
88
+ let e, i, s, l, t = (
89
+ /*item*/
90
+ a[3].caption && C(a)
91
+ );
92
+ return {
93
+ c() {
94
+ e = _("div"), i = _("video"), l = b(), t && t.c(), this.h();
95
+ },
96
+ l(n) {
97
+ e = u(n, "DIV", { class: !0 });
98
+ var r = v(e);
99
+ i = u(r, "VIDEO", { src: !0, preload: !0, class: !0 }), v(i).forEach(o), l = g(r), t && t.l(r), r.forEach(o), this.h();
100
+ },
101
+ h() {
102
+ p(i.src, s = /*item*/
103
+ a[3].video.url) || f(i, "src", s), i.controls = !1, i.muted = !0, f(i, "preload", "metadata"), f(i, "class", "svelte-1onbytl"), f(e, "class", "image-container svelte-1onbytl");
104
+ },
105
+ m(n, r) {
106
+ h(n, e, r), d(e, i), d(e, l), t && t.m(e, null);
107
+ },
108
+ p(n, r) {
109
+ r & /*value*/
110
+ 1 && !p(i.src, s = /*item*/
111
+ n[3].video.url) && f(i, "src", s), /*item*/
112
+ n[3].caption ? t ? t.p(n, r) : (t = C(n), t.c(), t.m(e, null)) : t && (t.d(1), t = null);
113
+ },
114
+ d(n) {
115
+ n && o(e), t && t.d();
116
+ }
117
+ };
118
+ }
119
+ function F(a) {
120
+ let e, i, s, l, t, n = (
121
+ /*item*/
122
+ a[3].caption && w(a)
123
+ );
124
+ return {
125
+ c() {
126
+ e = _("div"), i = _("img"), t = b(), n && n.c(), this.h();
127
+ },
128
+ l(r) {
129
+ e = u(r, "DIV", { class: !0 });
130
+ var c = v(e);
131
+ i = u(c, "IMG", { src: !0, alt: !0, class: !0 }), t = g(c), n && n.l(c), c.forEach(o), this.h();
132
+ },
133
+ h() {
134
+ p(i.src, s = /*item*/
135
+ a[3].image.url) || f(i, "src", s), f(i, "alt", l = /*item*/
136
+ a[3].caption || ""), f(i, "class", "svelte-1onbytl"), f(e, "class", "image-container svelte-1onbytl");
137
+ },
138
+ m(r, c) {
139
+ h(r, e, c), d(e, i), d(e, t), n && n.m(e, null);
140
+ },
141
+ p(r, c) {
142
+ c & /*value*/
143
+ 1 && !p(i.src, s = /*item*/
144
+ r[3].image.url) && f(i, "src", s), c & /*value*/
145
+ 1 && l !== (l = /*item*/
146
+ r[3].caption || "") && f(i, "alt", l), /*item*/
147
+ r[3].caption ? n ? n.p(r, c) : (n = w(r), n.c(), n.m(e, null)) : n && (n.d(1), n = null);
148
+ },
149
+ d(r) {
150
+ r && o(e), n && n.d();
151
+ }
152
+ };
153
+ }
154
+ function C(a) {
155
+ let e, i = (
156
+ /*item*/
157
+ a[3].caption + ""
158
+ ), s;
159
+ return {
160
+ c() {
161
+ e = _("span"), s = P(i), this.h();
162
+ },
163
+ l(l) {
164
+ e = u(l, "SPAN", { class: !0 });
165
+ var t = v(e);
166
+ s = q(t, i), t.forEach(o), this.h();
167
+ },
168
+ h() {
169
+ f(e, "class", "caption svelte-1onbytl");
170
+ },
171
+ m(l, t) {
172
+ h(l, e, t), d(e, s);
173
+ },
174
+ p(l, t) {
175
+ t & /*value*/
176
+ 1 && i !== (i = /*item*/
177
+ l[3].caption + "") && A(s, i);
178
+ },
179
+ d(l) {
180
+ l && o(e);
181
+ }
182
+ };
183
+ }
184
+ function w(a) {
185
+ let e, i = (
186
+ /*item*/
187
+ a[3].caption + ""
188
+ ), s;
189
+ return {
190
+ c() {
191
+ e = _("span"), s = P(i), this.h();
192
+ },
193
+ l(l) {
194
+ e = u(l, "SPAN", { class: !0 });
195
+ var t = v(e);
196
+ s = q(t, i), t.forEach(o), this.h();
197
+ },
198
+ h() {
199
+ f(e, "class", "caption svelte-1onbytl");
200
+ },
201
+ m(l, t) {
202
+ h(l, e, t), d(e, s);
203
+ },
204
+ p(l, t) {
205
+ t & /*value*/
206
+ 1 && i !== (i = /*item*/
207
+ l[3].caption + "") && A(s, i);
208
+ },
209
+ d(l) {
210
+ l && o(e);
211
+ }
212
+ };
213
+ }
214
+ function N(a) {
215
+ let e;
216
+ function i(t, n) {
217
+ if ("image" in /*item*/
218
+ t[3] && /*item*/
219
+ t[3].image) return F;
220
+ if ("video" in /*item*/
221
+ t[3] && /*item*/
222
+ t[3].video) return B;
223
+ }
224
+ let s = i(a), l = s && s(a);
225
+ return {
226
+ c() {
227
+ l && l.c(), e = k();
228
+ },
229
+ l(t) {
230
+ l && l.l(t), e = k();
231
+ },
232
+ m(t, n) {
233
+ l && l.m(t, n), h(t, e, n);
234
+ },
235
+ p(t, n) {
236
+ s === (s = i(t)) && l ? l.p(t, n) : (l && l.d(1), l = s && s(t), l && (l.c(), l.m(e.parentNode, e)));
237
+ },
238
+ d(t) {
239
+ t && o(e), l && l.d(t);
240
+ }
241
+ };
242
+ }
243
+ function S(a) {
244
+ let e, i = "…";
245
+ return {
246
+ c() {
247
+ e = _("div"), e.textContent = i, this.h();
248
+ },
249
+ l(s) {
250
+ e = u(s, "DIV", { class: !0, "data-svelte-h": !0 }), M(e) !== "svelte-1u6jrni" && (e.textContent = i), this.h();
251
+ },
252
+ h() {
253
+ f(e, "class", "more-indicator svelte-1onbytl");
254
+ },
255
+ m(s, l) {
256
+ h(s, e, l);
257
+ },
258
+ d(s) {
259
+ s && o(e);
260
+ }
261
+ };
262
+ }
263
+ function H(a) {
264
+ let e, i = (
265
+ /*value*/
266
+ a[0] && /*value*/
267
+ a[0].length > 0 && V(a)
268
+ );
269
+ return {
270
+ c() {
271
+ e = _("div"), i && i.c(), this.h();
272
+ },
273
+ l(s) {
274
+ e = u(s, "DIV", { class: !0 });
275
+ var l = v(e);
276
+ i && i.l(l), l.forEach(o), this.h();
277
+ },
278
+ h() {
279
+ f(e, "class", "container svelte-1onbytl"), m(
280
+ e,
281
+ "table",
282
+ /*type*/
283
+ a[1] === "table"
284
+ ), m(
285
+ e,
286
+ "gallery",
287
+ /*type*/
288
+ a[1] === "gallery"
289
+ ), m(
290
+ e,
291
+ "selected",
292
+ /*selected*/
293
+ a[2]
294
+ );
295
+ },
296
+ m(s, l) {
297
+ h(s, e, l), i && i.m(e, null);
298
+ },
299
+ p(s, [l]) {
300
+ /*value*/
301
+ s[0] && /*value*/
302
+ s[0].length > 0 ? i ? i.p(s, l) : (i = V(s), i.c(), i.m(e, null)) : i && (i.d(1), i = null), l & /*type*/
303
+ 2 && m(
304
+ e,
305
+ "table",
306
+ /*type*/
307
+ s[1] === "table"
308
+ ), l & /*type*/
309
+ 2 && m(
310
+ e,
311
+ "gallery",
312
+ /*type*/
313
+ s[1] === "gallery"
314
+ ), l & /*selected*/
315
+ 4 && m(
316
+ e,
317
+ "selected",
318
+ /*selected*/
319
+ s[2]
320
+ );
321
+ },
322
+ i: I,
323
+ o: I,
324
+ d(s) {
325
+ s && o(e), i && i.d();
326
+ }
327
+ };
328
+ }
329
+ function J(a, e, i) {
330
+ let { value: s } = e, { type: l } = e, { selected: t = !1 } = e;
331
+ return a.$$set = (n) => {
332
+ "value" in n && i(0, s = n.value), "type" in n && i(1, l = n.type), "selected" in n && i(2, t = n.selected);
333
+ }, [s, l, t];
334
+ }
335
+ class K extends j {
336
+ constructor(e) {
337
+ super(), O(this, e, J, H, z, { value: 0, type: 1, selected: 2 });
338
+ }
339
+ }
340
+ export {
341
+ K as default
342
+ };
src/backend/gradio_mediagallery/templates/example/style.css ADDED
@@ -0,0 +1 @@
 
 
1
+ .container.svelte-1onbytl.svelte-1onbytl{border-radius:var(--radius-lg);overflow:hidden}.container.selected.svelte-1onbytl.svelte-1onbytl{border:2px solid var(--border-color-accent)}.images-wrapper.svelte-1onbytl.svelte-1onbytl{display:flex;gap:var(--spacing-sm)}.container.table.svelte-1onbytl .images-wrapper.svelte-1onbytl{flex-direction:row;align-items:center;padding:var(--spacing-sm);border:1px solid var(--border-color-primary);border-radius:var(--radius-lg);background:var(--background-fill-secondary)}.container.gallery.svelte-1onbytl .images-wrapper.svelte-1onbytl{flex-direction:row;gap:0}.image-container.svelte-1onbytl.svelte-1onbytl{position:relative;flex-shrink:0}.container.table.svelte-1onbytl .image-container.svelte-1onbytl{width:var(--size-12);height:var(--size-12)}.container.gallery.svelte-1onbytl .image-container.svelte-1onbytl{width:var(--size-20);height:var(--size-20);margin-left:calc(-1 * var(--size-8))}.container.gallery.svelte-1onbytl .image-container.svelte-1onbytl:first-child{margin-left:0}.more-indicator.svelte-1onbytl.svelte-1onbytl{display:flex;align-items:center;justify-content:center;font-size:var(--text-lg);font-weight:700;color:var(--border-color-primary)}.container.table.svelte-1onbytl .more-indicator.svelte-1onbytl{width:var(--size-12);height:var(--size-12)}.container.gallery.svelte-1onbytl .more-indicator.svelte-1onbytl{width:var(--size-20);height:var(--size-20);margin-left:calc(-1 * var(--size-8));margin-right:calc(-1 * var(--size-6))}.image-container.svelte-1onbytl img.svelte-1onbytl,.image-container.svelte-1onbytl video.svelte-1onbytl{width:100%;height:100%;object-fit:cover;border-radius:var(--radius-md)}.caption.svelte-1onbytl.svelte-1onbytl{position:absolute;bottom:0;left:0;right:0;background:#000000b3;color:#fff;padding:var(--spacing-xs);font-size:var(--text-xs);text-align:center;border-radius:0 0 var(--radius-md) var(--radius-md);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.container.table.svelte-1onbytl .caption.svelte-1onbytl{display:none}
src/demo/__init__.py ADDED
File without changes
src/demo/app.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, List
2
+ import gradio as gr
3
+ from gradio_folderexplorer import FolderExplorer
4
+ from gradio_folderexplorer.helpers import load_media_from_folder
5
+ from gradio_mediagallery import MediaGallery
6
+ from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
7
+ import os
8
+
9
+ # Configuration constant for the root directory containing media files
10
+ ROOT_DIR_PATH = "./src/examples"
11
+
12
+ def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
13
+ """
14
+ Processes image metadata by calling the `transfer_metadata` helper.
15
+
16
+ Args:
17
+ image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
18
+
19
+ Returns:
20
+ List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
21
+ """
22
+ if not image_data or not hasattr(image_data, "_data"):
23
+ return [gr.skip()] * len(output_fields)
24
+
25
+ return transfer_metadata(
26
+ output_fields=output_fields,
27
+ metadata=image_data._data,
28
+ remove_prefix_from_keys=True
29
+ )
30
+
31
+ # UI layout and logic
32
+ with gr.Blocks() as demo:
33
+ """
34
+ A Gradio interface for browsing and displaying media files with metadata extraction.
35
+ """
36
+ gr.Markdown("# MediaGallery with Metadata Extraction")
37
+ gr.Markdown(
38
+ """
39
+ **To Test:**
40
+ 1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
41
+ 2. Click on an image in the **Media Gallery** to open the preview mode.
42
+ 3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
43
+ 4. Click the **'Load Metadata'** button inside the popup.
44
+ 5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
45
+ """
46
+ )
47
+ with gr.Row(equal_height=True):
48
+ with gr.Column(scale=1, min_width=300):
49
+ folder_explorer = FolderExplorer(
50
+ label="Select a Folder",
51
+ root_dir=ROOT_DIR_PATH,
52
+ value=ROOT_DIR_PATH
53
+ )
54
+
55
+ with gr.Column(scale=3):
56
+ gallery = MediaGallery(
57
+ label="Media in Folder",
58
+ columns=6,
59
+ height="auto",
60
+ preview=False,
61
+ show_download_button=False,
62
+ only_custom_metadata=False,
63
+ popup_metadata_width="40%",
64
+ )
65
+
66
+ gr.Markdown("## Metadata Viewer")
67
+ with gr.Row():
68
+ model_box = gr.Textbox(label="Model")
69
+ fnumber_box = gr.Textbox(label="FNumber")
70
+ iso_box = gr.Textbox(label="ISOSpeedRatings")
71
+ s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
72
+ description_box = gr.Textbox(label="Description", lines=2)
73
+
74
+ # Event handling
75
+ output_fields = [
76
+ model_box,
77
+ fnumber_box,
78
+ iso_box,
79
+ s_churn,
80
+ description_box
81
+ ]
82
+
83
+ # Populate the gallery when the folder changes
84
+ folder_explorer.change(
85
+ fn=load_media_from_folder,
86
+ inputs=folder_explorer,
87
+ outputs=gallery
88
+ )
89
+
90
+ # Populate the gallery on initial load
91
+ demo.load(
92
+ fn=load_media_from_folder,
93
+ inputs=folder_explorer,
94
+ outputs=gallery
95
+ )
96
+
97
+ # Handle the load_metadata event from MediaGallery
98
+ gallery.load_metadata(
99
+ fn=handle_load_metadata,
100
+ inputs=None,
101
+ outputs=output_fields
102
+ )
103
+
104
+ if __name__ == "__main__":
105
+ """
106
+ Launches the Gradio interface in debug mode.
107
+ """
108
+ demo.launch(debug=True)
src/demo/css.css ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html {
2
+ font-family: Inter;
3
+ font-size: 16px;
4
+ font-weight: 400;
5
+ line-height: 1.5;
6
+ -webkit-text-size-adjust: 100%;
7
+ background: #fff;
8
+ color: #323232;
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ text-rendering: optimizeLegibility;
12
+ }
13
+
14
+ :root {
15
+ --space: 1;
16
+ --vspace: calc(var(--space) * 1rem);
17
+ --vspace-0: calc(3 * var(--space) * 1rem);
18
+ --vspace-1: calc(2 * var(--space) * 1rem);
19
+ --vspace-2: calc(1.5 * var(--space) * 1rem);
20
+ --vspace-3: calc(0.5 * var(--space) * 1rem);
21
+ }
22
+
23
+ .app {
24
+ max-width: 748px !important;
25
+ }
26
+
27
+ .prose p {
28
+ margin: var(--vspace) 0;
29
+ line-height: var(--vspace * 2);
30
+ font-size: 1rem;
31
+ }
32
+
33
+ code {
34
+ font-family: "Inconsolata", sans-serif;
35
+ font-size: 16px;
36
+ }
37
+
38
+ h1,
39
+ h1 code {
40
+ font-weight: 400;
41
+ line-height: calc(2.5 / var(--space) * var(--vspace));
42
+ }
43
+
44
+ h1 code {
45
+ background: none;
46
+ border: none;
47
+ letter-spacing: 0.05em;
48
+ padding-bottom: 5px;
49
+ position: relative;
50
+ padding: 0;
51
+ }
52
+
53
+ h2 {
54
+ margin: var(--vspace-1) 0 var(--vspace-2) 0;
55
+ line-height: 1em;
56
+ }
57
+
58
+ h3,
59
+ h3 code {
60
+ margin: var(--vspace-1) 0 var(--vspace-2) 0;
61
+ line-height: 1em;
62
+ }
63
+
64
+ h4,
65
+ h5,
66
+ h6 {
67
+ margin: var(--vspace-3) 0 var(--vspace-3) 0;
68
+ line-height: var(--vspace);
69
+ }
70
+
71
+ .bigtitle,
72
+ h1,
73
+ h1 code {
74
+ font-size: calc(8px * 4.5);
75
+ word-break: break-word;
76
+ }
77
+
78
+ .title,
79
+ h2,
80
+ h2 code {
81
+ font-size: calc(8px * 3.375);
82
+ font-weight: lighter;
83
+ word-break: break-word;
84
+ border: none;
85
+ background: none;
86
+ }
87
+
88
+ .subheading1,
89
+ h3,
90
+ h3 code {
91
+ font-size: calc(8px * 1.8);
92
+ font-weight: 600;
93
+ border: none;
94
+ background: none;
95
+ letter-spacing: 0.1em;
96
+ text-transform: uppercase;
97
+ }
98
+
99
+ h2 code {
100
+ padding: 0;
101
+ position: relative;
102
+ letter-spacing: 0.05em;
103
+ }
104
+
105
+ blockquote {
106
+ font-size: calc(8px * 1.1667);
107
+ font-style: italic;
108
+ line-height: calc(1.1667 * var(--vspace));
109
+ margin: var(--vspace-2) var(--vspace-2);
110
+ }
111
+
112
+ .subheading2,
113
+ h4 {
114
+ font-size: calc(8px * 1.4292);
115
+ text-transform: uppercase;
116
+ font-weight: 600;
117
+ }
118
+
119
+ .subheading3,
120
+ h5 {
121
+ font-size: calc(8px * 1.2917);
122
+ line-height: calc(1.2917 * var(--vspace));
123
+
124
+ font-weight: lighter;
125
+ text-transform: uppercase;
126
+ letter-spacing: 0.15em;
127
+ }
128
+
129
+ h6 {
130
+ font-size: calc(8px * 1.1667);
131
+ font-size: 1.1667em;
132
+ font-weight: normal;
133
+ font-style: italic;
134
+ font-family: "le-monde-livre-classic-byol", serif !important;
135
+ letter-spacing: 0px !important;
136
+ }
137
+
138
+ #start .md > *:first-child {
139
+ margin-top: 0;
140
+ }
141
+
142
+ h2 + h3 {
143
+ margin-top: 0;
144
+ }
145
+
146
+ .md hr {
147
+ border: none;
148
+ border-top: 1px solid var(--block-border-color);
149
+ margin: var(--vspace-2) 0 var(--vspace-2) 0;
150
+ }
151
+ .prose ul {
152
+ margin: var(--vspace-2) 0 var(--vspace-1) 0;
153
+ }
154
+
155
+ .gap {
156
+ gap: 0;
157
+ }
src/demo/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio_mediagallery
2
+ gradio_folderexplorer
src/demo/space.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ from app import demo as app
4
+ import os
5
+
6
+ _docs = {'MediaGallery': {'description': 'Creates a gallery component that allows displaying a grid of images or videos, and optionally captions. If used as an input, the user can upload images or videos to the gallery.\nIf used as an output, the user can click on individual images or videos to view them at a higher resolution.\n', 'members': {'__init__': {'value': {'type': 'Sequence[\n np.ndarray | PIL.Image.Image | str | Path | tuple\n ]\n | Callable\n | None', 'default': 'None', 'description': 'List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.'}, 'file_types': {'type': 'list[str] | None', 'default': 'None', 'description': 'List of file extensions or types of files to be uploaded (e.g. [\'image\', \'.mp4\']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.'}, 'label': {'type': 'str | I18nData | None', 'default': 'None', 'description': 'the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'every': {'type': 'Timer | float | None', 'default': 'None', 'description': 'Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.'}, 'inputs': {'type': 'Component | Sequence[Component] | set[Component] | None', 'default': 'None', 'description': 'Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'visible': {'type': 'bool | Literal["hidden"]', 'default': 'True', 'description': 'If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'key': {'type': 'int | str | tuple[int | str, ...] | None', 'default': 'None', 'description': "in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render."}, 'preserved_by_key': {'type': 'list[str] | str | None', 'default': '"value"', 'description': "A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor."}, 'columns': {'type': 'int | None', 'default': '2', 'description': 'Represents the number of images that should be shown in one row.'}, 'rows': {'type': 'int | None', 'default': 'None', 'description': 'Represents the number of rows in the image grid.'}, 'height': {'type': 'int | float | str | None', 'default': 'None', 'description': 'The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.'}, 'allow_preview': {'type': 'bool', 'default': 'True', 'description': 'If True, images in the gallery will be enlarged when they are clicked. Default is True.'}, 'preview': {'type': 'bool | None', 'default': 'None', 'description': 'If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.'}, 'selected_index': {'type': 'int | None', 'default': 'None', 'description': 'The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.'}, 'object_fit': {'type': 'Literal[\n "contain", "cover", "fill", "none", "scale-down"\n ]\n | None', 'default': 'None', 'description': 'CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".'}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'show_download_button': {'type': 'bool | None', 'default': 'True', 'description': 'If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.'}, 'interactive': {'type': 'bool | None', 'default': 'None', 'description': 'If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.'}, 'type': {'type': 'Literal["numpy", "pil", "filepath"]', 'default': '"filepath"', 'description': 'The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.'}, 'show_fullscreen_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'only_custom_metadata': {'type': 'bool', 'default': 'True', 'description': 'If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.'}, 'popup_metadata_width': {'type': 'int | str', 'default': '500', 'description': 'The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").'}}, 'postprocess': {'value': {'type': 'list | None', 'description': "The output data received by the component from the user's function in the backend."}}, 'preprocess': {'return': {'type': 'Any', 'description': "The preprocessed input data sent to the user's function in the backend."}, 'value': None}}, 'events': {'select': {'type': None, 'default': None, 'description': 'Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'delete': {'type': None, 'default': None, 'description': 'This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data'}, 'preview_close': {'type': None, 'default': None, 'description': 'This event is triggered when the MediaGallery preview is closed by the user'}, 'preview_open': {'type': None, 'default': None, 'description': 'This event is triggered when the MediaGallery preview is opened by the user'}, 'load_metadata': {'type': None, 'default': None, 'description': "Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata."}}}, '__meta__': {'additional_interfaces': {}, 'user_fn_refs': {'MediaGallery': []}}}
7
+
8
+ abs_path = os.path.join(os.path.dirname(__file__), "css.css")
9
+
10
+ with gr.Blocks(
11
+ css=abs_path,
12
+ theme=gr.themes.Ocean(
13
+ font_mono=[
14
+ gr.themes.GoogleFont("Inconsolata"),
15
+ "monospace",
16
+ ],
17
+ ),
18
+ ) as demo:
19
+ gr.Markdown(
20
+ """
21
+ # `gradio_mediagallery`
22
+
23
+ <div style="display: flex; gap: 7px;">
24
+ <a href="https://pypi.org/project/gradio_mediagallery/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_mediagallery"></a>
25
+ </div>
26
+
27
+ Python library for easily interacting with trained machine learning models
28
+ """, elem_classes=["md-custom"], header_links=True)
29
+ app.render()
30
+ gr.Markdown(
31
+ """
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install gradio_mediagallery
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ from typing import Any, List
42
+ import gradio as gr
43
+ from gradio_folderexplorer import FolderExplorer
44
+ from gradio_folderexplorer.helpers import load_media_from_folder
45
+ from gradio_mediagallery import MediaGallery
46
+ from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
47
+ import os
48
+
49
+ # Configuration constant for the root directory containing media files
50
+ ROOT_DIR_PATH = "./src/examples"
51
+
52
+ def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
53
+ \"\"\"
54
+ Processes image metadata by calling the `transfer_metadata` helper.
55
+
56
+ Args:
57
+ image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
58
+
59
+ Returns:
60
+ List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
61
+ \"\"\"
62
+ if not image_data or not hasattr(image_data, "_data"):
63
+ return [gr.skip()] * len(output_fields)
64
+
65
+ return transfer_metadata(
66
+ output_fields=output_fields,
67
+ metadata=image_data._data,
68
+ remove_prefix_from_keys=True
69
+ )
70
+
71
+ # UI layout and logic
72
+ with gr.Blocks() as demo:
73
+ \"\"\"
74
+ A Gradio interface for browsing and displaying media files with metadata extraction.
75
+ \"\"\"
76
+ gr.Markdown("# MediaGallery with Metadata Extraction")
77
+ gr.Markdown(
78
+ \"\"\"
79
+ **To Test:**
80
+ 1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
81
+ 2. Click on an image in the **Media Gallery** to open the preview mode.
82
+ 3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
83
+ 4. Click the **'Load Metadata'** button inside the popup.
84
+ 5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
85
+ \"\"\"
86
+ )
87
+ with gr.Row(equal_height=True):
88
+ with gr.Column(scale=1, min_width=300):
89
+ folder_explorer = FolderExplorer(
90
+ label="Select a Folder",
91
+ root_dir=ROOT_DIR_PATH,
92
+ value=ROOT_DIR_PATH
93
+ )
94
+
95
+ with gr.Column(scale=3):
96
+ gallery = MediaGallery(
97
+ label="Media in Folder",
98
+ columns=6,
99
+ height="auto",
100
+ preview=False,
101
+ show_download_button=False,
102
+ only_custom_metadata=False,
103
+ popup_metadata_width="40%",
104
+ )
105
+
106
+ gr.Markdown("## Metadata Viewer")
107
+ with gr.Row():
108
+ model_box = gr.Textbox(label="Model")
109
+ fnumber_box = gr.Textbox(label="FNumber")
110
+ iso_box = gr.Textbox(label="ISOSpeedRatings")
111
+ s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
112
+ description_box = gr.Textbox(label="Description", lines=2)
113
+
114
+ # Event handling
115
+ output_fields = [
116
+ model_box,
117
+ fnumber_box,
118
+ iso_box,
119
+ s_churn,
120
+ description_box
121
+ ]
122
+
123
+ # Populate the gallery when the folder changes
124
+ folder_explorer.change(
125
+ fn=load_media_from_folder,
126
+ inputs=folder_explorer,
127
+ outputs=gallery
128
+ )
129
+
130
+ # Populate the gallery on initial load
131
+ demo.load(
132
+ fn=load_media_from_folder,
133
+ inputs=folder_explorer,
134
+ outputs=gallery
135
+ )
136
+
137
+ # Handle the load_metadata event from MediaGallery
138
+ gallery.load_metadata(
139
+ fn=handle_load_metadata,
140
+ inputs=None,
141
+ outputs=output_fields
142
+ )
143
+
144
+ if __name__ == "__main__":
145
+ \"\"\"
146
+ Launches the Gradio interface in debug mode.
147
+ \"\"\"
148
+ demo.launch(debug=True)
149
+ ```
150
+ """, elem_classes=["md-custom"], header_links=True)
151
+
152
+
153
+ gr.Markdown("""
154
+ ## `MediaGallery`
155
+
156
+ ### Initialization
157
+ """, elem_classes=["md-custom"], header_links=True)
158
+
159
+ gr.ParamViewer(value=_docs["MediaGallery"]["members"]["__init__"], linkify=[])
160
+
161
+
162
+ gr.Markdown("### Events")
163
+ gr.ParamViewer(value=_docs["MediaGallery"]["events"], linkify=['Event'])
164
+
165
+
166
+
167
+
168
+ gr.Markdown("""
169
+
170
+ ### User function
171
+
172
+ The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
173
+
174
+ - When used as an Input, the component only impacts the input signature of the user function.
175
+ - When used as an output, the component only impacts the return signature of the user function.
176
+
177
+ The code snippet below is accurate in cases where the component is used as both an input and an output.
178
+
179
+ - **As input:** Is passed, the preprocessed input data sent to the user's function in the backend.
180
+ - **As output:** Should return, the output data received by the component from the user's function in the backend.
181
+
182
+ ```python
183
+ def predict(
184
+ value: Any
185
+ ) -> list | None:
186
+ return value
187
+ ```
188
+ """, elem_classes=["md-custom", "MediaGallery-user-fn"], header_links=True)
189
+
190
+
191
+
192
+
193
+ demo.load(None, js=r"""function() {
194
+ const refs = {};
195
+ const user_fn_refs = {
196
+ MediaGallery: [], };
197
+ requestAnimationFrame(() => {
198
+
199
+ Object.entries(user_fn_refs).forEach(([key, refs]) => {
200
+ if (refs.length > 0) {
201
+ const el = document.querySelector(`.${key}-user-fn`);
202
+ if (!el) return;
203
+ refs.forEach(ref => {
204
+ el.innerHTML = el.innerHTML.replace(
205
+ new RegExp("\\b"+ref+"\\b", "g"),
206
+ `<a href="#h-${ref.toLowerCase()}">${ref}</a>`
207
+ );
208
+ })
209
+ }
210
+ })
211
+
212
+ Object.entries(refs).forEach(([key, refs]) => {
213
+ if (refs.length > 0) {
214
+ const el = document.querySelector(`.${key}`);
215
+ if (!el) return;
216
+ refs.forEach(ref => {
217
+ el.innerHTML = el.innerHTML.replace(
218
+ new RegExp("\\b"+ref+"\\b", "g"),
219
+ `<a href="#h-${ref.toLowerCase()}">${ref}</a>`
220
+ );
221
+ })
222
+ }
223
+ })
224
+ })
225
+ }
226
+
227
+ """)
228
+
229
+ demo.launch()
src/examples/folder1/Jensen.jpeg ADDED

Git LFS Details

  • SHA256: 31fd1d52ee41559dcda55e304aef19df2767ff1c76295480d838210868fb63a5
  • Pointer size: 131 Bytes
  • Size of remote file: 166 kB
src/examples/folder1/newton_0.jpg ADDED

Git LFS Details

  • SHA256: 312532bd8fa2222662aa6807569c710df63640ff00d8984639c358af1128c2ff
  • Pointer size: 131 Bytes
  • Size of remote file: 318 kB
src/examples/folder1/newton_2.png ADDED

Git LFS Details

  • SHA256: 63ed6b340b8923d1b24bb97ed56b5f007e7a938aaf25e19a2100e93b885309f4
  • Pointer size: 132 Bytes
  • Size of remote file: 1.48 MB
src/examples/folder1/newton_3.jpg ADDED
src/examples/folder1/sam1.jpg ADDED

Git LFS Details

  • SHA256: c17bc45b822ba2b5b74a8cf29d0f4eaeda638a9be4bd27838dc7012f1012a9ad
  • Pointer size: 131 Bytes
  • Size of remote file: 326 kB
src/examples/folder2/SampleVideo 720x480.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:91a5373da22bc8f34aa5664588b4728f88eeddb42abbfff362c7eb822a2b94cb
3
+ size 186695
src/examples/folder2/butterfly_input.jpg ADDED

Git LFS Details

  • SHA256: d7a7904c4891cc478039abc832289882845a26a09b93e1688fc7918f08b9195e
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
src/examples/folder2/lemons_input.jpg ADDED

Git LFS Details

  • SHA256: 55e3d5d6984bb74dcfaa527102f75f9df048d70224811cfcb5d710731205918c
  • Pointer size: 131 Bytes
  • Size of remote file: 281 kB
src/examples/folder2/vermeer.jpg ADDED
src/examples/image_with_meta.png ADDED

Git LFS Details

  • SHA256: 0ee3f0bc8525aaa355a495f9cfadf00306261971d389b0291233e514c12f1d40
  • Pointer size: 132 Bytes
  • Size of remote file: 5.79 MB
src/frontend/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>
src/frontend/Gallery.css ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :global(*::-webkit-scrollbar) {
2
+ width: 8px;
3
+ height: 8px;
4
+ background-color: transparent;
5
+ }
6
+
7
+ :global(*::-webkit-scrollbar-track) {
8
+ background: transparent;
9
+ border-radius: 10px;
10
+ }
11
+
12
+ :global(*::-webkit-scrollbar-thumb) {
13
+ background-color: rgba(136, 136, 136, 0.4);
14
+ border-radius: 10px;
15
+ border: 2px solid transparent;
16
+ background-clip: content-box;
17
+ }
18
+
19
+ :global(*::-webkit-scrollbar-thumb:hover) {
20
+ background-color: rgba(136, 136, 136, 0.7);
21
+ }
22
+
23
+ /* Para Firefox */
24
+ :global(html) {
25
+ scrollbar-width: thin;
26
+ scrollbar-color: rgba(136, 136, 136, 0.7) transparent;
27
+ }
28
+
29
+ /**
30
+ * Styles for the gallery container, which holds the entire component.
31
+ */
32
+ .gallery-container {
33
+ position: relative;
34
+ width: 100%;
35
+ height: 100%;
36
+ }
37
+
38
+ /**
39
+ * Styles for buttons to ensure they fill their container.
40
+ */
41
+ button {
42
+ width: var(--size-full);
43
+ height: var(--size-full);
44
+ display: block;
45
+ border-radius: var(--radius-lg);
46
+ }
47
+
48
+ /**
49
+ * Styles for the preview mode, displaying a selected media item.
50
+ */
51
+ .preview {
52
+ display: flex;
53
+ position: absolute;
54
+ flex-direction: column;
55
+ z-index: var(--layer-2);
56
+ border-radius: calc(var(--block-radius) - var(--block-border-width));
57
+ -webkit-backdrop-filter: blur(8px);
58
+ backdrop-filter: blur(8px);
59
+ width: var(--size-full);
60
+ height: var(--size-full);
61
+ }
62
+
63
+ .preview:focus-visible {
64
+ outline: none;
65
+ }
66
+
67
+ .preview.minimal {
68
+ width: fit-content;
69
+ height: fit-content;
70
+ }
71
+
72
+ .preview::before {
73
+ content: "";
74
+ position: absolute;
75
+ z-index: var(--layer-below);
76
+ background: var(--background-fill-primary);
77
+ opacity: 0.9;
78
+ width: var(--size-full);
79
+ height: var(--size-full);
80
+ }
81
+
82
+ /**
83
+ * Styles for the grid wrapper with fixed height constraints.
84
+ */
85
+ .fixed-height {
86
+ min-height: var(--size-80);
87
+ max-height: 65vh;
88
+ }
89
+
90
+ @media (--screen-xl) {
91
+ .fixed-height {
92
+ min-height: 450px;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Styles for the media container in preview mode.
98
+ */
99
+ .media-container {
100
+ height: calc(100% - var(--size-14));
101
+ width: 100%;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ cursor: pointer;
106
+ }
107
+
108
+ .media-container :global(img),
109
+ .media-container :global(video) {
110
+ max-width: 100%;
111
+ max-height: 100%;
112
+ object-fit: contain;
113
+ }
114
+
115
+ /**
116
+ * Styles for thumbnails in the preview mode carousel.
117
+ */
118
+ .thumbnails :global(img) {
119
+ object-fit: cover;
120
+ width: var(--size-full);
121
+ height: var(--size-full);
122
+ }
123
+
124
+ .thumbnails :global(svg) {
125
+ position: absolute;
126
+ top: var(--size-2);
127
+ left: var(--size-2);
128
+ width: 50%;
129
+ height: 50%;
130
+ opacity: 50%;
131
+ }
132
+
133
+ /**
134
+ * Styles for captions in preview mode.
135
+ */
136
+ .caption {
137
+ padding: var(--size-2) var(--size-3);
138
+ overflow: hidden;
139
+ color: var(--block-label-text-color);
140
+ font-weight: var(--weight-semibold);
141
+ text-align: center;
142
+ text-overflow: ellipsis;
143
+ white-space: nowrap;
144
+ align-self: center;
145
+ }
146
+
147
+ /**
148
+ * Styles for the thumbnails carousel in preview mode.
149
+ */
150
+ .thumbnails {
151
+ display: flex;
152
+ position: absolute;
153
+ bottom: 0;
154
+ justify-content: flex-start;
155
+ align-items: center;
156
+ gap: var(--spacing-lg);
157
+ width: var(--size-full);
158
+ height: var(--size-14);
159
+ overflow-x: scroll;
160
+ }
161
+
162
+ /**
163
+ * Styles for individual thumbnail items.
164
+ */
165
+ .thumbnail-item {
166
+ --ring-color: transparent;
167
+ position: relative;
168
+ box-shadow: inset 0 0 0 1px var(--ring-color), var(--shadow-drop);
169
+ border: 1px solid var(--border-color-primary);
170
+ border-radius: var(--button-small-radius);
171
+ background: var(--background-fill-secondary);
172
+ aspect-ratio: var(--ratio-square);
173
+ width: var(--size-full);
174
+ height: var(--size-full);
175
+ overflow: clip;
176
+ }
177
+
178
+ .thumbnail-item:hover {
179
+ --ring-color: var(--color-accent);
180
+ border-color: var(--color-accent);
181
+ filter: brightness(1.1);
182
+ }
183
+
184
+ .thumbnail-item.selected {
185
+ --ring-color: var(--color-accent);
186
+ border-color: var(--color-accent);
187
+ }
188
+
189
+ .thumbnail-item :global(svg) {
190
+ position: absolute;
191
+ top: 50%;
192
+ left: 50%;
193
+ width: 50%;
194
+ height: 50%;
195
+ opacity: 50%;
196
+ transform: translate(-50%, -50%);
197
+ }
198
+
199
+ .thumbnail-item :global(video) {
200
+ width: var(--size-full);
201
+ height: var(--size-full);
202
+ overflow: hidden;
203
+ object-fit: cover;
204
+ }
205
+
206
+ /**
207
+ * Styles for small thumbnails in the preview carousel.
208
+ */
209
+ .thumbnail-small {
210
+ flex: none;
211
+ transform: scale(0.9);
212
+ transition: 0.075s;
213
+ width: var(--size-9);
214
+ height: var(--size-9);
215
+ }
216
+
217
+ .thumbnail-small.selected {
218
+ --ring-color: var(--color-accent);
219
+ transform: scale(1);
220
+ border-color: var(--color-accent);
221
+ }
222
+
223
+ /**
224
+ * Styles for the grid wrapper containing the gallery items.
225
+ */
226
+ .grid-wrap {
227
+ position: relative;
228
+ padding: var(--size-2);
229
+ overflow-y: auto;
230
+ }
231
+
232
+ .grid-wrap.fixed-height {
233
+ min-height: var(--size-80);
234
+ max-height: 65vh;
235
+ }
236
+
237
+ /**
238
+ * Styles for the grid container for multiple items.
239
+ */
240
+ .grid-container {
241
+ display: grid;
242
+ position: relative;
243
+ grid-template-rows: repeat(var(--grid-rows), minmax(100px, 1fr));
244
+ grid-template-columns: repeat(var(--grid-cols), minmax(100px, 1fr));
245
+ grid-auto-rows: minmax(100px, 1fr);
246
+ gap: var(--spacing-lg);
247
+ }
248
+
249
+ /**
250
+ * Styles for single-item view wrapper.
251
+ */
252
+ .single-item-wrapper {
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ width: 100%;
257
+ height: 100%;
258
+ padding: var(--spacing-xxl);
259
+ box-sizing: border-box;
260
+ }
261
+
262
+ .single-item-wrapper .gallery-item-with-name {
263
+ width: 100%;
264
+ height: 100%;
265
+ max-width: min(300px, 80vw);
266
+ max-height: min(320px, calc(80vh - var(--size-4)));
267
+ display: flex;
268
+ flex-direction: column;
269
+ align-items: center;
270
+ }
271
+
272
+ .single-item-wrapper .gallery-item {
273
+ width: 100%;
274
+ height: 100%;
275
+ max-width: 100%;
276
+ max-height: 100%;
277
+ }
278
+
279
+ .single-item-wrapper .thumbnail-item.thumbnail-lg {
280
+ display: flex !important;
281
+ align-items: center !important;
282
+ justify-content: center !important;
283
+ }
284
+
285
+ .single-item-wrapper .thumbnail-filename {
286
+ height: var(--size-4);
287
+ line-height: var(--size-4);
288
+ }
289
+
290
+ .single-item-wrapper .thumbnail-lg > :global(img),
291
+ .single-item-wrapper .thumbnail-lg > :global(video) {
292
+ object-fit: var(--object-fit) !important;
293
+ }
294
+
295
+ /**
296
+ * Styles for large thumbnails in the grid or single-item view.
297
+ */
298
+ .thumbnail-lg > :global(img),
299
+ .thumbnail-lg > :global(video) {
300
+ width: var(--size-full);
301
+ height: var(--size-full);
302
+ overflow: hidden;
303
+ object-fit: var(--object-fit);
304
+ }
305
+
306
+ .thumbnail-lg:hover .caption-label {
307
+ opacity: 0.5;
308
+ }
309
+
310
+ /**
311
+ * Styles for captions in the grid or single-item view.
312
+ */
313
+ .caption-label {
314
+ position: absolute;
315
+ right: var(--block-label-margin);
316
+ bottom: var(--block-label-margin);
317
+ z-index: var(--layer-1);
318
+ border-top: 1px solid var(--border-color-primary);
319
+ border-left: 1px solid var(--border-color-primary);
320
+ border-radius: var(--block-label-radius);
321
+ background: var(--background-fill-secondary);
322
+ padding: var(--block-label-padding);
323
+ max-width: 80%;
324
+ overflow: hidden;
325
+ font-size: var(--block-label-text-size);
326
+ text-align: left;
327
+ text-overflow: ellipsis;
328
+ white-space: nowrap;
329
+ }
330
+
331
+ .grid-wrap.minimal {
332
+ padding: 0;
333
+ }
334
+
335
+ /**
336
+ * Styles for gallery items with associated filenames.
337
+ */
338
+ .gallery-item-with-name {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: var(--size-1);
342
+ width: 100%;
343
+ height: 100%;
344
+ }
345
+
346
+ .thumbnail-filename {
347
+ font-size: var(--text-xs);
348
+ color: var(--body-text-color);
349
+ text-align: center;
350
+ width: 100%;
351
+ overflow: hidden;
352
+ text-overflow: ellipsis;
353
+ white-space: nowrap;
354
+ padding: 0 var(--size-1);
355
+ }
356
+
357
+ .gallery-item {
358
+ position: relative;
359
+ width: 100%;
360
+ height: 100%;
361
+ }
362
+
363
+ /**
364
+ * Styles for the metadata popup displayed in preview mode.
365
+ */
366
+ .metadata-popup {
367
+ position: fixed;
368
+ top: 50%;
369
+ left: 50%;
370
+ transform: translate(-50%, -50%);
371
+ background: var(--background-fill-primary, white);
372
+ border: 1px solid var(--border-color-primary);
373
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
374
+ z-index: 1000;
375
+ border-radius: 8px;
376
+ max-width: min(90%, 600px);
377
+ max-height: min(50vh, calc(100% - 2rem));
378
+ min-height: 200px;
379
+ display: flex;
380
+ flex-direction: column;
381
+ pointer-events: auto;
382
+ }
383
+
384
+ .popup-content {
385
+ padding: 1rem;
386
+ display: flex;
387
+ flex-direction: column;
388
+ width: 100%;
389
+ box-sizing: border-box;
390
+ overflow-y: auto;
391
+ position: relative;
392
+ }
393
+
394
+ .close-button {
395
+ position: absolute;
396
+ top: 0.5rem;
397
+ right: 0.5rem;
398
+ background: none;
399
+ border: none;
400
+ font-size: 1.25rem;
401
+ cursor: pointer;
402
+ z-index: 20;
403
+ color: var(--body-text-color);
404
+ padding: 0.25rem;
405
+ line-height: 1;
406
+ width: 24px;
407
+ height: 24px;
408
+ text-align: center;
409
+ }
410
+
411
+ .popup-title {
412
+ font-weight: bold;
413
+ margin: 0 0 1rem 0;
414
+ flex-shrink: 0;
415
+ padding-right: 2.5rem;
416
+ }
417
+
418
+ .metadata-table-container {
419
+ flex-grow: 1;
420
+ overflow-y: auto;
421
+ max-height: calc(100% - 5rem);
422
+ min-height: 0;
423
+ margin-bottom: 1rem;
424
+ }
425
+
426
+ .metadata-table {
427
+ width: 100%;
428
+ border-collapse: collapse;
429
+ table-layout: auto;
430
+ }
431
+
432
+ .metadata-label {
433
+ background: var(--background-fill-secondary, #f5f5f5);
434
+ padding: 0.5rem;
435
+ font-weight: bold;
436
+ text-align: left;
437
+ vertical-align: top;
438
+ width: 35%;
439
+ }
440
+
441
+ .metadata-value {
442
+ text-align: left;
443
+ padding: 0.5rem;
444
+ white-space: pre-wrap;
445
+ word-break: break-all;
446
+ vertical-align: top;
447
+ }
448
+
449
+ .load-metadata-button {
450
+ margin-top: 1rem;
451
+ padding: 0.5rem 1rem;
452
+ background-color: var(--button-primary-background-fill);
453
+ color: var(--button-primary-text-color);
454
+ border: none;
455
+ border-radius: 4px;
456
+ cursor: pointer;
457
+ align-self: center;
458
+ flex-shrink: 0;
459
+ }
460
+
461
+ .load-metadata-button:hover {
462
+ background-color: var(--button-primary-background-fill-hover);
463
+ }
464
+
465
+ .no-metadata-message {
466
+ flex-grow: 1;
467
+ display: flex;
468
+ align-items: center;
469
+ justify-content: center;
470
+ color: var(--body-text-color-subdued);
471
+ }
src/frontend/Index.svelte ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script context="module" lang="ts">
2
+ export { default as BaseGallery } from "./shared/Gallery.svelte";
3
+ export { default as BaseExample } from "./Example.svelte";
4
+ </script>
5
+
6
+ <script lang="ts">
7
+ import type { GalleryImage, GalleryVideo } from "./types";
8
+ import type { Gradio, SelectData } from "@gradio/utils";
9
+ import { Block, Empty } from "@gradio/atoms";
10
+ import Gallery from "./shared/Gallery.svelte";
11
+ import type { LoadingStatus } from "@gradio/statustracker";
12
+ import { StatusTracker } from "@gradio/statustracker";
13
+ import { Image } from "@gradio/icons";
14
+
15
+ type GalleryData = GalleryImage | GalleryVideo;
16
+
17
+ export let loading_status: LoadingStatus;
18
+ export let show_label: boolean;
19
+ export let label: string;
20
+ export let elem_id = "";
21
+ export let elem_classes: string[] = [];
22
+ export let visible: boolean | "hidden" = true;
23
+ // O `value` agora é uma lista simples de arquivos
24
+ export let value: GalleryData[] | null = null;
25
+ export let container = true;
26
+ export let scale: number | null = null;
27
+ export let min_width: number | undefined = undefined;
28
+ export let columns: number | number[] | undefined = [5];
29
+ export let height: number | "auto" = "auto";
30
+ export let preview: boolean = true;
31
+ export let allow_preview = true;
32
+ export let selected_index: number | null = null;
33
+ export let object_fit: "contain" | "cover" | "fill" | "none" | "scale-down" = "cover";
34
+ export let gradio: Gradio<{
35
+ change: typeof value;
36
+ select: SelectData;
37
+ share: ShareData;
38
+ error: string;
39
+ prop_change: Record<string, any>;
40
+ clear_status: LoadingStatus;
41
+ preview_open: never;
42
+ preview_close: never;
43
+ load_metadata: Record<string, any>;
44
+ }>;
45
+ export let show_fullscreen_button = true;
46
+ export let show_download_button = false;
47
+ export let show_share_button = false;
48
+ export let fullscreen = false;
49
+ export let popup_metadata_width: number | string = "50%";
50
+ import "./Gallery.css";
51
+ </script>
52
+
53
+ <Block
54
+ {visible}
55
+ variant={value === null || value.length === 0 ? "dashed" : "solid"}
56
+ padding={false}
57
+ {elem_id}
58
+ {elem_classes}
59
+ {container}
60
+ {scale}
61
+ {min_width}
62
+ allow_overflow={false}
63
+ height={typeof height === "number" ? height : undefined}
64
+ bind:fullscreen
65
+ >
66
+ <StatusTracker
67
+ autoscroll={gradio.autoscroll}
68
+ i18n={gradio.i18n}
69
+ {...loading_status}
70
+ />
71
+ {#if value === null || value.length === 0}
72
+ <Empty unpadded_box={true} size="large"><Image /></Empty>
73
+ {:else}
74
+ <Gallery
75
+ on:change={() => gradio.dispatch("change", value)}
76
+ on:select={(e) => gradio.dispatch("select", e.detail)}
77
+ on:share={(e) => gradio.dispatch("share", e.detail)}
78
+ on:error={(e) => gradio.dispatch("error", e.detail)}
79
+ on:preview_open={() => gradio.dispatch("preview_open")}
80
+ on:preview_close={() => gradio.dispatch("preview_close")}
81
+ on:fullscreen={({ detail }) => {
82
+ fullscreen = detail;
83
+ }}
84
+ on:load_metadata={(e) => gradio.dispatch("load_metadata", e.detail)}
85
+ {label}
86
+ {show_label}
87
+ {columns}
88
+ height={"auto"}
89
+ {preview}
90
+ {object_fit}
91
+ {allow_preview}
92
+ bind:selected_index
93
+ bind:value
94
+ i18n={gradio.i18n}
95
+ _fetch={(...args) => gradio.client.fetch(...args)}
96
+ {show_fullscreen_button}
97
+ {show_download_button}
98
+ {show_share_button}
99
+ {fullscreen}
100
+ {popup_metadata_width}
101
+
102
+ />
103
+ {/if}
104
+ </Block>
105
+
src/frontend/gradio.config.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: [],
3
+ svelte: {
4
+ preprocess: [],
5
+ },
6
+ build: {
7
+ target: "modules",
8
+ },
9
+ };
src/frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
src/frontend/package.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gradio_mediagallery",
3
+ "version": "0.15.34",
4
+ "description": "Gradio UI packages",
5
+ "type": "module",
6
+ "author": "",
7
+ "license": "ISC",
8
+ "private": false,
9
+ "dependencies": {
10
+ "@gradio/atoms": "0.18.1",
11
+ "@gradio/client": "1.19.1",
12
+ "@gradio/file": "0.13.0",
13
+ "@gradio/icons": "0.14.0",
14
+ "@gradio/image": "0.23.1",
15
+ "@gradio/statustracker": "0.11.1",
16
+ "@gradio/upload": "0.17.1",
17
+ "@gradio/utils": "0.10.2",
18
+ "@gradio/video": "0.16.0",
19
+ "dequal": "^2.0.2",
20
+ "exifr": "^7.1.3",
21
+ "marked": "^16.3.0"
22
+ },
23
+ "devDependencies": {
24
+ "@gradio/preview": "0.14.0"
25
+ },
26
+ "main": "./Index.svelte",
27
+ "main_changeset": true,
28
+ "exports": {
29
+ ".": {
30
+ "gradio": "./Index.svelte",
31
+ "svelte": "./dist/Index.svelte",
32
+ "types": "./dist/Index.svelte.d.ts"
33
+ },
34
+ "./package.json": "./package.json",
35
+ "./base": {
36
+ "gradio": "./shared/Gallery.svelte",
37
+ "svelte": "./dist/shared/Gallery.svelte",
38
+ "types": "./dist/shared/Gallery.svelte.d.ts"
39
+ },
40
+ "./example": {
41
+ "gradio": "./Example.svelte",
42
+ "svelte": "./dist/Example.svelte",
43
+ "types": "./dist/Example.svelte.d.ts"
44
+ }
45
+ },
46
+ "peerDependencies": {
47
+ "svelte": "^4.0.0"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/gradio-app/gradio.git",
52
+ "directory": "js/gallery"
53
+ }
54
+ }
src/frontend/shared/Gallery.svelte ADDED
@@ -0,0 +1,1099 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import {
3
+ BlockLabel,
4
+ Empty,
5
+ ShareButton,
6
+ IconButton,
7
+ IconButtonWrapper,
8
+ FullscreenButton,
9
+ } from "@gradio/atoms";
10
+ import type { SelectData } from "@gradio/utils";
11
+ import { Image } from "@gradio/image/shared";
12
+ import { Video } from "@gradio/video/shared";
13
+ import { dequal } from "dequal";
14
+ import { createEventDispatcher, onMount } from "svelte";
15
+ import { tick } from "svelte";
16
+ import type { GalleryImage, GalleryVideo } from "../types";
17
+ import { Download, Image as ImageIcon, Clear, Play, Info } from "@gradio/icons";
18
+ import { FileData } from "@gradio/client";
19
+ import { format_gallery_for_sharing } from "./utils";
20
+ import type { I18nFormatter } from "@gradio/utils";
21
+ import * as exifr from "exifr";
22
+
23
+ type GalleryData = GalleryImage | GalleryVideo;
24
+
25
+ /**
26
+ * @component Gallery
27
+ * @description A Svelte component for displaying a gallery of images or videos with optional preview mode, fullscreen support, and metadata popup for images.
28
+ */
29
+
30
+ // Component props
31
+ /** @prop {boolean} show_label - Whether to display the gallery label. Defaults to true. */
32
+ export let show_label = true;
33
+ /** @prop {string} label - The label text for the gallery. */
34
+ export let label: string;
35
+ /** @prop {GalleryData[] | null} value - Array of gallery items (images or videos). */
36
+ export let value: GalleryData[] | null = null;
37
+ /** @prop {number | number[] | undefined} columns - Number of grid columns or array of column counts per breakpoint. Defaults to [2]. */
38
+ export let columns: number | number[] | undefined = [2];
39
+ /** @prop {number | number[] | undefined} rows - Number of grid rows or array of row counts per breakpoint. */
40
+ export let rows: number | number[] | undefined = undefined;
41
+ /** @prop {number | "auto"} height - Gallery height in pixels or "auto". Defaults to "auto". */
42
+ export let height: number | "auto" = "auto";
43
+ /** @prop {boolean} preview - Whether to start in preview mode if a value is provided. */
44
+ export let preview: boolean;
45
+ /** @prop {boolean} allow_preview - Whether preview mode is enabled. Defaults to true. */
46
+ export let allow_preview = true;
47
+ /** @prop {"contain" | "cover" | "fill" | "none" | "scale-down"} object_fit - CSS object-fit for media. Defaults to "cover". */
48
+ export let object_fit: "contain" | "cover" | "fill" | "none" | "scale-down" = "cover";
49
+ /** @prop {boolean} show_share_button - Whether to show the share button. Defaults to false. */
50
+ export let show_share_button = false;
51
+ /** @prop {boolean} show_download_button - Whether to show the download button. Defaults to false. */
52
+ export let show_download_button = false;
53
+ /** @prop {I18nFormatter} i18n - Internationalization formatter for labels. */
54
+ export let i18n: I18nFormatter;
55
+ /** @prop {number | null} selected_index - Index of the selected media item. Defaults to null. */
56
+ export let selected_index: number | null = null;
57
+ /** @prop {boolean} interactive - Whether the gallery is interactive (not used). Defaults to false. */
58
+ export const interactive = false;
59
+ /** @prop {typeof fetch} _fetch - Fetch function for downloading files. */
60
+ export let _fetch: typeof fetch;
61
+ /** @prop {"normal" | "minimal"} mode - Display mode for the gallery. Defaults to "normal". */
62
+ export let mode: "normal" | "minimal" = "normal";
63
+ /** @prop {boolean} show_fullscreen_button - Whether to show the fullscreen button. Defaults to true. */
64
+ export let show_fullscreen_button = true;
65
+ /** @prop {boolean} display_icon_button_wrapper_top_corner - Whether to position icon buttons in the top corner. Defaults to false. */
66
+ export let display_icon_button_wrapper_top_corner = false;
67
+ /** @prop {boolean} fullscreen - Whether the gallery is in fullscreen mode. Defaults to false. */
68
+ export let fullscreen = false;
69
+ /** @prop {boolean} only_custom_metadata - Whether to show only custom metadata in the popup. Defaults to true. */
70
+ export let only_custom_metadata: boolean = true;
71
+ /** @prop {number | string} popup_metadata_width - Width of the metadata popup. Defaults to "50%". */
72
+ export let popup_metadata_width: number | string = "50%";
73
+
74
+
75
+ const dispatch = createEventDispatcher<{
76
+ change: undefined;
77
+ select: SelectData;
78
+ preview_open: undefined;
79
+ preview_close: undefined;
80
+ fullscreen: boolean;
81
+ error: string;
82
+ load_metadata: Record<string, any>;
83
+ }>();
84
+
85
+ // Gallery state
86
+ let is_full_screen = false;
87
+ let image_container: HTMLElement;
88
+ let was_reset = true;
89
+ let resolved_value: GalleryData[] | null = null;
90
+ let effective_columns: number | number[] | undefined = columns;
91
+ let prev_value: GalleryData[] | null = value;
92
+ let old_selected_index: number | null = selected_index;
93
+ let el: HTMLButtonElement[] = [];
94
+ let container_element: HTMLDivElement;
95
+ let thumbnails_overflow = false;
96
+ let preview_element: HTMLDivElement | null = null;
97
+
98
+ // Metadata state
99
+ let metadata: Record<string, any> | null = null;
100
+ let showMetadataPopup: boolean = false;
101
+ let is_extracting_metadata = false;
102
+ const technicalMetadata: string[] = [
103
+ "ImageWidth",
104
+ "ImageHeight",
105
+ "BitDepth",
106
+ "ColorType",
107
+ "Compression",
108
+ "Filter",
109
+ "Interlace",
110
+ ];
111
+
112
+ $: filteredMetadata = only_custom_metadata && metadata
113
+ ? Object.fromEntries(
114
+ Object.entries(metadata).filter(([key]) => !technicalMetadata.includes(key))
115
+ )
116
+ : metadata;
117
+
118
+ /**
119
+ * Extracts metadata from an image file using the exifr library.
120
+ * @param fileData - The file data containing the image URL.
121
+ * @returns A promise resolving to the extracted metadata or null if extraction fails or is not applicable.
122
+ */
123
+ async function extractMetadata(fileData: FileData): Promise<Record<string, any> | null> {
124
+ if (!fileData?.url) return null;
125
+ if (
126
+ fileData.url.toLowerCase().endsWith(".png") ||
127
+ fileData.url.toLowerCase().endsWith(".jpg") ||
128
+ fileData.url.toLowerCase().endsWith(".jpeg")
129
+ ) {
130
+ try {
131
+ const data = await exifr.parse(fileData.url, true);
132
+ let parsed_meta: Record<string, any> = {};
133
+ if (data) {
134
+ for (const [key, value] of Object.entries(data)) {
135
+ if (
136
+ typeof value === "string" ||
137
+ typeof value === "number" ||
138
+ typeof value === "boolean"
139
+ ) {
140
+ parsed_meta[key] = value;
141
+ }
142
+ }
143
+ }
144
+ return parsed_meta;
145
+ } catch (error) {
146
+ return {};
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Toggles the metadata popup visibility and extracts metadata if needed.
154
+ */
155
+ async function toggleMetadataPopup(): Promise<void> {
156
+ if (showMetadataPopup) {
157
+ showMetadataPopup = false;
158
+ return;
159
+ }
160
+ if (!selected_media) return;
161
+ const media_file = "image" in selected_media ? selected_media.image : null;
162
+ if (!media_file) return;
163
+ is_extracting_metadata = true;
164
+ metadata = await extractMetadata(media_file);
165
+ is_extracting_metadata = false;
166
+ showMetadataPopup = true;
167
+ }
168
+
169
+ /**
170
+ * Dispatches the load_metadata event with filtered metadata and closes the popup.
171
+ */
172
+ function dispatchLoadMetadata(): void {
173
+ if (filteredMetadata !== null) {
174
+ dispatch("load_metadata", filteredMetadata);
175
+ closePopup();
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Closes the metadata popup.
181
+ */
182
+ function closePopup(): void {
183
+ showMetadataPopup = false;
184
+ }
185
+
186
+ $: was_reset = value == null || value.length === 0;
187
+ $: resolved_value = value
188
+ ? (value.map((data) =>
189
+ "video" in data
190
+ ? { video: data.video as FileData, caption: data.caption }
191
+ : { image: data.image as FileData, caption: data.caption }
192
+ ) as GalleryData[])
193
+ : null;
194
+
195
+ $: {
196
+ if (resolved_value && columns) {
197
+ const item_count = resolved_value.length;
198
+ if (Array.isArray(columns)) {
199
+ effective_columns = columns.map((col) => Math.min(col, item_count));
200
+ } else {
201
+ effective_columns = Math.min(columns, item_count);
202
+ }
203
+ } else {
204
+ effective_columns = columns;
205
+ }
206
+ }
207
+
208
+ $: if (!dequal(prev_value, value)) {
209
+ selected_index = null;
210
+ if (preview && value && value.length > 0) {
211
+ selected_index = 0;
212
+ }
213
+ dispatch("change");
214
+ prev_value = value;
215
+ }
216
+
217
+ $: selected_media =
218
+ selected_index != null && resolved_value != null
219
+ ? resolved_value[selected_index]
220
+ : null;
221
+
222
+ $: has_extractable_metadata =
223
+ selected_media &&
224
+ "image" in selected_media &&
225
+ (selected_media.image.url?.toLowerCase().endsWith(".png") ||
226
+ selected_media.image.url?.toLowerCase().endsWith(".jpg") ||
227
+ selected_media.image.url?.toLowerCase().endsWith(".jpeg"));
228
+
229
+ $: previous = ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) % (resolved_value?.length ?? 0);
230
+ $: next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
231
+
232
+ /**
233
+ * Handles click events on the preview image to navigate to the previous or next item.
234
+ * @param event - The mouse click event.
235
+ */
236
+ function handle_preview_click(event: MouseEvent): void {
237
+ const element = event.target as HTMLElement;
238
+ const x = event.offsetX;
239
+ const centerX = element.offsetWidth / 2;
240
+ selected_index = x < centerX ? previous : next;
241
+ }
242
+
243
+ /**
244
+ * Handles keyboard navigation in preview mode.
245
+ * @param e - The keyboard event.
246
+ */
247
+ function on_keydown(e: KeyboardEvent): void {
248
+ switch (e.code) {
249
+ case "Escape":
250
+ e.preventDefault();
251
+ selected_index = null;
252
+ dispatch("preview_close");
253
+ break;
254
+ case "ArrowLeft":
255
+ e.preventDefault();
256
+ selected_index = previous;
257
+ break;
258
+ case "ArrowRight":
259
+ e.preventDefault();
260
+ selected_index = next;
261
+ break;
262
+ }
263
+ }
264
+
265
+ $: {
266
+ if (selected_index !== old_selected_index) {
267
+ showMetadataPopup = false;
268
+ metadata = null;
269
+ old_selected_index = selected_index;
270
+ if (selected_index !== null) {
271
+ if (resolved_value != null) {
272
+ selected_index = Math.max(0, Math.min(selected_index, resolved_value.length - 1));
273
+ }
274
+ dispatch("select", {
275
+ index: selected_index,
276
+ value: resolved_value?.[selected_index],
277
+ });
278
+ }
279
+ }
280
+ }
281
+
282
+ $: if (allow_preview) {
283
+ scroll_to_img(selected_index);
284
+ }
285
+
286
+ $: if (selected_index !== null && preview_element) {
287
+ tick().then(() => {
288
+ preview_element?.focus();
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Scrolls to the selected thumbnail in the thumbnails container.
294
+ * @param index - The index of the thumbnail to scroll to.
295
+ */
296
+ async function scroll_to_img(index: number | null): Promise<void> {
297
+ if (typeof index !== "number" || !container_element) return;
298
+ await tick();
299
+ if (!el[index]) return;
300
+ el[index].focus();
301
+ const { left: container_left, width: container_width } = container_element.getBoundingClientRect();
302
+ const { left, width } = el[index].getBoundingClientRect();
303
+ const pos = left - container_left + width / 2 - container_width / 2 + container_element.scrollLeft;
304
+ container_element.scrollTo({ left: pos < 0 ? 0 : pos, behavior: "smooth" });
305
+ }
306
+
307
+ /**
308
+ * Downloads a file from the provided URL.
309
+ * @param file_url - The URL of the file to download.
310
+ * @param name - The name to use for the downloaded file.
311
+ */
312
+ async function download(file_url: string, name: string): Promise<void> {
313
+ try {
314
+ const response = await _fetch(file_url);
315
+ const blob = await response.blob();
316
+ const url = URL.createObjectURL(blob);
317
+ const link = document.createElement("a");
318
+ link.href = url;
319
+ link.download = name;
320
+ link.click();
321
+ URL.revokeObjectURL(url);
322
+ } catch (error) {
323
+ if (error instanceof TypeError) {
324
+ window.open(file_url, "_blank", "noreferrer");
325
+ return;
326
+ }
327
+ throw error;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Checks if the thumbnails container overflows horizontally.
333
+ */
334
+ function check_thumbnails_overflow(): void {
335
+ if (container_element) {
336
+ thumbnails_overflow = container_element.scrollWidth > container_element.clientWidth;
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Initializes the component, setting up event listeners for fullscreen and resize events.
342
+ */
343
+ onMount(() => {
344
+ check_thumbnails_overflow();
345
+ document.addEventListener("fullscreenchange", () => {
346
+ is_full_screen = !!document.fullscreenElement;
347
+ fullscreen = is_full_screen;
348
+ });
349
+ window.addEventListener("resize", check_thumbnails_overflow);
350
+ return () => {
351
+ window.removeEventListener("resize", check_thumbnails_overflow);
352
+ document.removeEventListener("fullscreenchange", () => {
353
+ is_full_screen = !!document.fullscreenElement;
354
+ fullscreen = is_full_screen;
355
+ });
356
+ };
357
+ });
358
+
359
+ $: resolved_value, check_thumbnails_overflow();
360
+ $: if (container_element) check_thumbnails_overflow();
361
+ </script>
362
+
363
+ <svelte:window />
364
+
365
+ {#if show_label}
366
+ <BlockLabel {show_label} Icon={ImageIcon} label={label || "Gallery"} />
367
+ {/if}
368
+
369
+ {#if value == null || resolved_value == null || resolved_value.length === 0}
370
+ <Empty unpadded_box={true} size="large"><ImageIcon /></Empty>
371
+ {:else}
372
+ <div class="gallery-container" bind:this={image_container}>
373
+ <!-- Preview Mode -->
374
+ {#if selected_media && allow_preview}
375
+ <button
376
+ class="preview"
377
+ bind:this={preview_element}
378
+ class:minimal={mode === "minimal"}
379
+ aria-label="Image Preview"
380
+ tabindex="-1"
381
+ on:keydown={on_keydown}
382
+ >
383
+ <IconButtonWrapper {display_icon_button_wrapper_top_corner}>
384
+ {#if show_download_button}
385
+ <IconButton
386
+ Icon={Download}
387
+ label={i18n("common.download")}
388
+ on:click={() => {
389
+ const media =
390
+ "image" in selected_media
391
+ ? selected_media.image
392
+ : selected_media.video;
393
+ if (media?.url) download(media.url, media.orig_name ?? "media");
394
+ }}
395
+ />
396
+ {/if}
397
+ {#if show_fullscreen_button}
398
+ <FullscreenButton
399
+ {fullscreen}
400
+ on:fullscreen={() => {
401
+ fullscreen = !fullscreen;
402
+ if (fullscreen) {
403
+ preview_element?.requestFullscreen();
404
+ } else if (document.fullscreenElement) {
405
+ document.exitFullscreen();
406
+ }
407
+ }}
408
+ />
409
+ {/if}
410
+ {#if has_extractable_metadata}
411
+ <IconButton
412
+ Icon={Info}
413
+ label="View Metadata"
414
+ pending={is_extracting_metadata}
415
+ on:click={(event) => {
416
+ event.stopPropagation();
417
+ toggleMetadataPopup();
418
+ }}
419
+ />
420
+ {/if}
421
+ {#if show_share_button}
422
+ <div class="icon-button">
423
+ <ShareButton
424
+ {i18n}
425
+ on:share
426
+ on:error
427
+ {value}
428
+ formatter={format_gallery_for_sharing}
429
+ />
430
+ </div>
431
+ {/if}
432
+ {#if !is_full_screen}
433
+ <IconButton
434
+ Icon={Clear}
435
+ label="Close"
436
+ on:click={() => {
437
+ selected_index = null;
438
+ dispatch("preview_close");
439
+ }}
440
+ />
441
+ {/if}
442
+ </IconButtonWrapper>
443
+
444
+ <button
445
+ class="media-container"
446
+ on:click={"image" in selected_media ? handle_preview_click : null}
447
+ >
448
+ {#if "image" in selected_media}
449
+ <Image
450
+ src={selected_media.image.url}
451
+ alt={selected_media.caption || ""}
452
+ loading="lazy"
453
+ />
454
+ {:else}
455
+ <Video
456
+ src={selected_media.video.url}
457
+ alt={selected_media.caption || ""}
458
+ loading="lazy"
459
+ controls={true}
460
+ />
461
+ {/if}
462
+ </button>
463
+
464
+ {#if selected_media?.caption}
465
+ <caption class="caption">{selected_media.caption}</caption>
466
+ {/if}
467
+
468
+ <div
469
+ bind:this={container_element}
470
+ class="thumbnails scroll-hide"
471
+ style="justify-content: {thumbnails_overflow ? 'flex-start' : 'center'};"
472
+ >
473
+ {#each resolved_value as media, i}
474
+ <button
475
+ bind:this={el[i]}
476
+ on:click={() => (selected_index = i)}
477
+ class="thumbnail-item thumbnail-small"
478
+ class:selected={selected_index === i && mode !== "minimal"}
479
+ aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
480
+ >
481
+ {#if "image" in media}
482
+ <Image
483
+ src={media.image.url}
484
+ title={media.caption || null}
485
+ alt=""
486
+ loading="lazy"
487
+ />
488
+ {:else}
489
+ <Play />
490
+ <Video
491
+ src={media.video.url}
492
+ title={media.caption || null}
493
+ is_stream={false}
494
+ alt=""
495
+ loading="lazy"
496
+ loop={false}
497
+ />
498
+ {/if}
499
+ </button>
500
+ {/each}
501
+ </div>
502
+
503
+ {#if showMetadataPopup && filteredMetadata !== null}
504
+ <div
505
+ class="metadata-popup"
506
+ on:click|stopPropagation
507
+ role="presentation"
508
+ style:width={typeof popup_metadata_width === "number" ? `${popup_metadata_width}px` : popup_metadata_width}
509
+ >
510
+ <div class="popup-content">
511
+ <button class="close-button" on:click={closePopup}>X</button>
512
+ <h3 class="popup-title">Image Metadata</h3>
513
+ {#if Object.keys(filteredMetadata).length > 0}
514
+ <div class="metadata-table-container">
515
+ <table class="metadata-table">
516
+ <tbody>
517
+ {#each Object.entries(filteredMetadata) as [key, val]}
518
+ {#if val}
519
+ <tr>
520
+ <td class="metadata-label">{key}</td>
521
+ <td class="metadata-value">{val}</td>
522
+ </tr>
523
+ {/if}
524
+ {/each}
525
+ </tbody>
526
+ </table>
527
+ </div>
528
+ <button
529
+ class="load-metadata-button"
530
+ on:click={dispatchLoadMetadata}
531
+ >Load Metadata</button>
532
+ {:else}
533
+ <p class="no-metadata-message">No custom metadata found.</p>
534
+ {/if}
535
+ </div>
536
+ </div>
537
+ {/if}
538
+ </button>
539
+ {/if}
540
+
541
+ <!-- Main Grid / Single Item View -->
542
+ <div
543
+ class="grid-wrap"
544
+ class:minimal={mode === "minimal"}
545
+ class:fixed-height={!height || height == "auto"}
546
+ class:hidden={is_full_screen || (selected_media && allow_preview)}
547
+ style:height={height !== "auto" ? `${height}px` : null}
548
+ >
549
+ <!-- Multi-item grid -->
550
+ {#if resolved_value && resolved_value.length > 1}
551
+ <div
552
+ class="grid-container"
553
+ style:--grid-cols={effective_columns}
554
+ style:--grid-rows={rows}
555
+ style:--object-fit={object_fit}
556
+ class:pt-6={show_label}
557
+ >
558
+ {#each resolved_value as entry, i}
559
+ {@const file_name = "image" in entry ? entry.image.orig_name : entry.video.orig_name}
560
+ <div class="gallery-item-with-name">
561
+ <div class="gallery-item">
562
+ <button
563
+ class="thumbnail-item thumbnail-lg"
564
+ class:selected={selected_index === i}
565
+ on:click={() => {
566
+ if (selected_index === null && allow_preview)
567
+ dispatch("preview_open");
568
+ selected_index = i;
569
+ }}
570
+ aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
571
+ >
572
+ {#if "image" in entry}
573
+ <Image
574
+ alt={entry.caption || ""}
575
+ src={entry.image.url}
576
+ loading="lazy"
577
+ />
578
+ {:else}
579
+ <Play />
580
+ <Video
581
+ src={entry.video.url}
582
+ title={entry.caption || null}
583
+ is_stream={false}
584
+ alt=""
585
+ loading="lazy"
586
+ loop={false}
587
+ />
588
+ {/if}
589
+ {#if entry.caption}
590
+ <div class="caption-label">{entry.caption}</div>
591
+ {/if}
592
+ </button>
593
+ </div>
594
+ {#if file_name}
595
+ <div class="thumbnail-filename" title={file_name}>
596
+ {file_name}
597
+ </div>
598
+ {/if}
599
+ </div>
600
+ {/each}
601
+ </div>
602
+ <!-- Single-item view -->
603
+ {:else if resolved_value && resolved_value.length === 1}
604
+ {@const entry = resolved_value[0]}
605
+ {@const file_name = "image" in entry ? entry.image.orig_name : entry.video.orig_name}
606
+ <div class="single-item-wrapper" style:--object-fit={object_fit}>
607
+ <div class="gallery-item-with-name">
608
+ <div class="gallery-item">
609
+ <button
610
+ class="thumbnail-item thumbnail-lg"
611
+ on:click={() => {
612
+ if (allow_preview) {
613
+ dispatch("preview_open");
614
+ selected_index = 0;
615
+ }
616
+ }}
617
+ aria-label="View single item in preview mode"
618
+ >
619
+ {#if "image" in entry}
620
+ <Image
621
+ alt={entry.caption || ""}
622
+ src={entry.image.url}
623
+ loading="lazy"
624
+ />
625
+ {:else}
626
+ <Play />
627
+ <Video
628
+ src={entry.video.url}
629
+ title={entry.caption || null}
630
+ is_stream={false}
631
+ alt=""
632
+ loading="lazy"
633
+ loop={false}
634
+ />
635
+ {/if}
636
+ {#if entry.caption}
637
+ <div class="caption-label">{entry.caption}</div>
638
+ {/if}
639
+ </button>
640
+ </div>
641
+ {#if file_name}
642
+ <div class="thumbnail-filename" title={file_name}>
643
+ {file_name}
644
+ </div>
645
+ {/if}
646
+ </div>
647
+ </div>
648
+ {/if}
649
+ </div>
650
+ </div>
651
+ {/if}
652
+
653
+ <style lang="postcss">
654
+ /**
655
+ * Styles for the gallery container, which holds the entire component.
656
+ */
657
+ .gallery-container {
658
+ position: relative;
659
+ width: 100%;
660
+ height: 100%;
661
+ }
662
+
663
+ /**
664
+ * Styles for buttons to ensure they fill their container.
665
+ */
666
+ button {
667
+ width: var(--size-full);
668
+ height: var(--size-full);
669
+ display: block;
670
+ border-radius: var(--radius-lg);
671
+ }
672
+
673
+ /**
674
+ * Styles for the preview mode, displaying a selected media item.
675
+ */
676
+ .preview {
677
+ display: flex;
678
+ position: absolute;
679
+ flex-direction: column;
680
+ z-index: var(--layer-2);
681
+ border-radius: calc(var(--block-radius) - var(--block-border-width));
682
+ -webkit-backdrop-filter: blur(8px);
683
+ backdrop-filter: blur(8px);
684
+ width: var(--size-full);
685
+ height: var(--size-full);
686
+ }
687
+
688
+ .preview:focus-visible {
689
+ outline: none;
690
+ }
691
+
692
+ .preview.minimal {
693
+ width: fit-content;
694
+ height: fit-content;
695
+ }
696
+
697
+ .preview::before {
698
+ content: "";
699
+ position: absolute;
700
+ z-index: var(--layer-below);
701
+ background: var(--background-fill-primary);
702
+ opacity: 0.9;
703
+ width: var(--size-full);
704
+ height: var(--size-full);
705
+ }
706
+
707
+ /**
708
+ * Styles for the grid wrapper with fixed height constraints.
709
+ */
710
+ .fixed-height {
711
+ min-height: var(--size-80);
712
+ max-height: 65vh;
713
+ }
714
+
715
+ @media (--screen-xl) {
716
+ .fixed-height {
717
+ min-height: 450px;
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Styles for the media container in preview mode.
723
+ */
724
+ .media-container {
725
+ height: calc(100% - var(--size-14));
726
+ width: 100%;
727
+ display: flex;
728
+ align-items: center;
729
+ justify-content: center;
730
+ cursor: pointer;
731
+ }
732
+
733
+ .media-container :global(img),
734
+ .media-container :global(video) {
735
+ max-width: 100%;
736
+ max-height: 100%;
737
+ object-fit: contain;
738
+ }
739
+
740
+ /**
741
+ * Styles for thumbnails in the preview mode carousel.
742
+ */
743
+ .thumbnails :global(img) {
744
+ object-fit: cover;
745
+ width: var(--size-full);
746
+ height: var(--size-full);
747
+ }
748
+
749
+ .thumbnails :global(svg) {
750
+ position: absolute;
751
+ top: var(--size-2);
752
+ left: var(--size-2);
753
+ width: 50%;
754
+ height: 50%;
755
+ opacity: 50%;
756
+ }
757
+
758
+ /**
759
+ * Styles for captions in preview mode.
760
+ */
761
+ .caption {
762
+ padding: var(--size-2) var(--size-3);
763
+ overflow: hidden;
764
+ color: var(--block-label-text-color);
765
+ font-weight: var(--weight-semibold);
766
+ text-align: center;
767
+ text-overflow: ellipsis;
768
+ white-space: nowrap;
769
+ align-self: center;
770
+ }
771
+
772
+ /**
773
+ * Styles for the thumbnails carousel in preview mode.
774
+ */
775
+ .thumbnails {
776
+ display: flex;
777
+ position: absolute;
778
+ bottom: 0;
779
+ justify-content: flex-start;
780
+ align-items: center;
781
+ gap: var(--spacing-lg);
782
+ width: var(--size-full);
783
+ height: var(--size-14);
784
+ overflow-x: scroll;
785
+ }
786
+
787
+ /**
788
+ * Styles for individual thumbnail items.
789
+ */
790
+ .thumbnail-item {
791
+ --ring-color: transparent;
792
+ position: relative;
793
+ box-shadow: inset 0 0 0 1px var(--ring-color), var(--shadow-drop);
794
+ border: 1px solid var(--border-color-primary);
795
+ border-radius: var(--button-small-radius);
796
+ background: var(--background-fill-secondary);
797
+ aspect-ratio: var(--ratio-square);
798
+ width: var(--size-full);
799
+ height: var(--size-full);
800
+ overflow: clip;
801
+ }
802
+
803
+ .thumbnail-item:hover {
804
+ --ring-color: var(--color-accent);
805
+ border-color: var(--color-accent);
806
+ filter: brightness(1.1);
807
+ }
808
+
809
+ .thumbnail-item.selected {
810
+ --ring-color: var(--color-accent);
811
+ border-color: var(--color-accent);
812
+ }
813
+
814
+ .thumbnail-item :global(svg) {
815
+ position: absolute;
816
+ top: 50%;
817
+ left: 50%;
818
+ width: 50%;
819
+ height: 50%;
820
+ opacity: 50%;
821
+ transform: translate(-50%, -50%);
822
+ }
823
+
824
+ .thumbnail-item :global(video) {
825
+ width: var(--size-full);
826
+ height: var(--size-full);
827
+ overflow: hidden;
828
+ object-fit: cover;
829
+ }
830
+
831
+ /**
832
+ * Styles for small thumbnails in the preview carousel.
833
+ */
834
+ .thumbnail-small {
835
+ flex: none;
836
+ transform: scale(0.9);
837
+ transition: 0.075s;
838
+ width: var(--size-9);
839
+ height: var(--size-9);
840
+ }
841
+
842
+ .thumbnail-small.selected {
843
+ --ring-color: var(--color-accent);
844
+ transform: scale(1);
845
+ border-color: var(--color-accent);
846
+ }
847
+
848
+ /**
849
+ * Styles for the grid wrapper containing the gallery items.
850
+ */
851
+ .grid-wrap {
852
+ position: relative;
853
+ padding: var(--size-2);
854
+ overflow-y: auto;
855
+ }
856
+
857
+ .grid-wrap.fixed-height {
858
+ min-height: var(--size-80);
859
+ max-height: 65vh;
860
+ }
861
+
862
+ /**
863
+ * Styles for the grid container for multiple items.
864
+ */
865
+ .grid-container {
866
+ display: grid;
867
+ position: relative;
868
+ grid-template-rows: repeat(var(--grid-rows), minmax(100px, 1fr));
869
+ grid-template-columns: repeat(var(--grid-cols), minmax(100px, 1fr));
870
+ grid-auto-rows: minmax(100px, 1fr);
871
+ gap: var(--spacing-lg);
872
+ }
873
+
874
+ /**
875
+ * Styles for single-item view wrapper.
876
+ */
877
+ .single-item-wrapper {
878
+ display: flex;
879
+ align-items: center;
880
+ justify-content: center;
881
+ width: 100%;
882
+ height: 100%;
883
+ padding: var(--spacing-xxl);
884
+ box-sizing: border-box;
885
+ }
886
+
887
+ .single-item-wrapper .gallery-item-with-name {
888
+ width: 100%;
889
+ height: 100%;
890
+ max-width: min(300px, 80vw);
891
+ max-height: min(320px, calc(80vh - var(--size-4)));
892
+ display: flex;
893
+ flex-direction: column;
894
+ align-items: center;
895
+ }
896
+
897
+ .single-item-wrapper .gallery-item {
898
+ width: 100%;
899
+ height: 100%;
900
+ max-width: 100%;
901
+ max-height: 100%;
902
+ }
903
+
904
+ .single-item-wrapper .thumbnail-item.thumbnail-lg {
905
+ display: flex !important;
906
+ align-items: center !important;
907
+ justify-content: center !important;
908
+ }
909
+
910
+ .single-item-wrapper .thumbnail-filename {
911
+ height: var(--size-4);
912
+ line-height: var(--size-4);
913
+ }
914
+
915
+ .single-item-wrapper .thumbnail-lg > :global(img),
916
+ .single-item-wrapper .thumbnail-lg > :global(video) {
917
+ object-fit: var(--object-fit) !important;
918
+ }
919
+
920
+ /**
921
+ * Styles for large thumbnails in the grid or single-item view.
922
+ */
923
+ .thumbnail-lg > :global(img),
924
+ .thumbnail-lg > :global(video) {
925
+ width: var(--size-full);
926
+ height: var(--size-full);
927
+ overflow: hidden;
928
+ object-fit: var(--object-fit);
929
+ }
930
+
931
+ .thumbnail-lg:hover .caption-label {
932
+ opacity: 0.5;
933
+ }
934
+
935
+ /**
936
+ * Styles for captions in the grid or single-item view.
937
+ */
938
+ .caption-label {
939
+ position: absolute;
940
+ right: var(--block-label-margin);
941
+ bottom: var(--block-label-margin);
942
+ z-index: var(--layer-1);
943
+ border-top: 1px solid var(--border-color-primary);
944
+ border-left: 1px solid var(--border-color-primary);
945
+ border-radius: var(--block-label-radius);
946
+ background: var(--background-fill-secondary);
947
+ padding: var(--block-label-padding);
948
+ max-width: 80%;
949
+ overflow: hidden;
950
+ font-size: var(--block-label-text-size);
951
+ text-align: left;
952
+ text-overflow: ellipsis;
953
+ white-space: nowrap;
954
+ }
955
+
956
+ .grid-wrap.minimal {
957
+ padding: 0;
958
+ }
959
+
960
+ /**
961
+ * Styles for gallery items with associated filenames.
962
+ */
963
+ .gallery-item-with-name {
964
+ display: flex;
965
+ flex-direction: column;
966
+ gap: var(--size-1);
967
+ width: 100%;
968
+ height: 100%;
969
+ }
970
+
971
+ .thumbnail-filename {
972
+ font-size: var(--text-xs);
973
+ color: var(--body-text-color);
974
+ text-align: center;
975
+ width: 100%;
976
+ overflow: hidden;
977
+ text-overflow: ellipsis;
978
+ white-space: nowrap;
979
+ padding: 0 var(--size-1);
980
+ }
981
+
982
+ .gallery-item {
983
+ position: relative;
984
+ width: 100%;
985
+ height: 100%;
986
+ }
987
+
988
+ /**
989
+ * Styles for the metadata popup displayed in preview mode.
990
+ */
991
+ .metadata-popup {
992
+ position: fixed;
993
+ top: 50%;
994
+ left: 50%;
995
+ transform: translate(-50%, -50%);
996
+ background: var(--background-fill-primary, white);
997
+ border: 1px solid var(--border-color-primary);
998
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
999
+ z-index: 1000;
1000
+ border-radius: 8px;
1001
+ max-width: min(90%, 600px);
1002
+ max-height: min(50vh, calc(100% - 2rem));
1003
+ min-height: 200px;
1004
+ display: flex;
1005
+ flex-direction: column;
1006
+ pointer-events: auto;
1007
+ }
1008
+
1009
+ .popup-content {
1010
+ padding: 1rem;
1011
+ display: flex;
1012
+ flex-direction: column;
1013
+ width: 100%;
1014
+ box-sizing: border-box;
1015
+ overflow-y: auto;
1016
+ position: relative;
1017
+ }
1018
+
1019
+ .close-button {
1020
+ position: absolute;
1021
+ top: 0.5rem;
1022
+ right: 0.5rem;
1023
+ background: none;
1024
+ border: none;
1025
+ font-size: 1.25rem;
1026
+ cursor: pointer;
1027
+ z-index: 20;
1028
+ color: var(--body-text-color);
1029
+ padding: 0.25rem;
1030
+ line-height: 1;
1031
+ width: 24px;
1032
+ height: 24px;
1033
+ text-align: center;
1034
+ }
1035
+
1036
+ .popup-title {
1037
+ font-weight: bold;
1038
+ margin: 0 0 1rem 0;
1039
+ flex-shrink: 0;
1040
+ padding-right: 2.5rem;
1041
+ }
1042
+
1043
+ .metadata-table-container {
1044
+ flex-grow: 1;
1045
+ overflow-y: auto;
1046
+ max-height: calc(100% - 5rem);
1047
+ min-height: 0;
1048
+ margin-bottom: 1rem;
1049
+ }
1050
+
1051
+ .metadata-table {
1052
+ width: 100%;
1053
+ border-collapse: collapse;
1054
+ table-layout: auto;
1055
+ }
1056
+
1057
+ .metadata-label {
1058
+ background: var(--background-fill-secondary, #f5f5f5);
1059
+ padding: 0.5rem;
1060
+ font-weight: bold;
1061
+ text-align: left;
1062
+ vertical-align: top;
1063
+ width: 35%;
1064
+ }
1065
+
1066
+ .metadata-value {
1067
+ text-align: left;
1068
+ padding: 0.5rem;
1069
+ white-space: pre-wrap;
1070
+ word-break: break-all;
1071
+ vertical-align: top;
1072
+ }
1073
+
1074
+ .load-metadata-button {
1075
+ margin-top: 1rem;
1076
+ padding: 0.5rem 1rem;
1077
+ background-color: var(--button-primary-background-fill);
1078
+ color: var(--button-primary-text-color);
1079
+ border: none;
1080
+ border-radius: 4px;
1081
+ cursor: pointer;
1082
+ align-self: center;
1083
+ flex-shrink: 0;
1084
+ }
1085
+
1086
+ .load-metadata-button:hover {
1087
+ background-color: var(--button-primary-background-fill-hover);
1088
+ }
1089
+
1090
+ .no-metadata-message {
1091
+ flex-grow: 1;
1092
+ display: flex;
1093
+ align-items: center;
1094
+ justify-content: center;
1095
+ color: var(--body-text-color-subdued);
1096
+ }
1097
+
1098
+
1099
+ </style>
src/frontend/shared/utils.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { uploadToHuggingFace } from "@gradio/utils";
2
+ import type { FileData } from "@gradio/client";
3
+
4
+ export async function format_gallery_for_sharing(
5
+ value: [FileData, string | null][] | null
6
+ ): Promise<string> {
7
+ if (!value) return "";
8
+ let urls = await Promise.all(
9
+ value.map(async ([image, _]) => {
10
+ if (image === null || !image.url) return "";
11
+ return await uploadToHuggingFace(image.url, "url");
12
+ })
13
+ );
14
+
15
+ return `<div style="display: flex; flex-wrap: wrap; gap: 16px">${urls
16
+ .map((url) => `<img src="${url}" style="height: 400px" />`)
17
+ .join("")}</div>`;
18
+ }
src/frontend/tsconfig.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "checkJs": true,
5
+ "esModuleInterop": true,
6
+ "forceConsistentCasingInFileNames": true,
7
+ "resolveJsonModule": true,
8
+ "skipLibCheck": true,
9
+ "sourceMap": true,
10
+ "strict": true,
11
+ "verbatimModuleSyntax": true
12
+ },
13
+ "exclude": ["node_modules", "dist", "./gradio.config.js"]
14
+ }
src/frontend/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
+ }
src/log.txt ADDED
File without changes
src/pyproject.toml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = [
3
+ "hatchling",
4
+ "hatch-requirements-txt",
5
+ "hatch-fancy-pypi-readme>=22.5.0",
6
+ ]
7
+ build-backend = "hatchling.build"
8
+
9
+ [project]
10
+ name = "gradio_mediagallery"
11
+ version = "0.0.1"
12
+ description = "Python library for easily interacting with trained machine learning models"
13
+ readme = "README.md"
14
+ license = "Apache-2.0"
15
+ requires-python = ">=3.8"
16
+ authors = [{ name = "Eliseu Silva", email = "elismasilva@gmail.com" }]
17
+ keywords = [
18
+ "gradio-custom-component",
19
+ "gradio-template-Gallery"
20
+ ]
21
+ # Add dependencies here
22
+ dependencies = ["gradio>=4.0,<6.0"]
23
+ classifiers = [
24
+ 'Development Status :: 3 - Alpha',
25
+ 'Operating System :: OS Independent',
26
+ 'Programming Language :: Python :: 3',
27
+ 'Programming Language :: Python :: 3 :: Only',
28
+ 'Programming Language :: Python :: 3.8',
29
+ 'Programming Language :: Python :: 3.9',
30
+ 'Programming Language :: Python :: 3.10',
31
+ 'Programming Language :: Python :: 3.11',
32
+ 'Topic :: Scientific/Engineering',
33
+ 'Topic :: Scientific/Engineering :: Artificial Intelligence',
34
+ 'Topic :: Scientific/Engineering :: Visualization',
35
+ ]
36
+
37
+ # The repository and space URLs are optional, but recommended.
38
+ # Adding a repository URL will create a badge in the auto-generated README that links to the repository.
39
+ # Adding a space URL will create a badge in the auto-generated README that links to the space.
40
+ # This will make it easy for people to find your deployed demo or source code when they
41
+ # encounter your project in the wild.
42
+
43
+ # [project.urls]
44
+ # repository = "your github repository"
45
+ # space = "your space url"
46
+
47
+ [project.optional-dependencies]
48
+ dev = ["build", "twine"]
49
+
50
+ [tool.hatch.build]
51
+ artifacts = ["/backend/gradio_mediagallery/templates", "*.pyi", "/\\backend\\gradio_mediagallery\\templates"]
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["/backend/gradio_mediagallery"]