initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
This commit is contained in:
commit
6a73d11c4b
124 changed files with 34856 additions and 0 deletions
663
crates/pinakes-ui/src/components/detail.rs
Normal file
663
crates/pinakes-ui/src/components/detail.rs
Normal file
|
|
@ -0,0 +1,663 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
use super::image_viewer::ImageViewer;
|
||||
use super::markdown_viewer::MarkdownViewer;
|
||||
use super::media_player::MediaPlayer;
|
||||
use super::utils::{format_duration, format_size, media_category, type_badge_class};
|
||||
use crate::client::{MediaResponse, MediaUpdateEvent, TagResponse};
|
||||
|
||||
#[component]
|
||||
pub fn Detail(
|
||||
media: MediaResponse,
|
||||
media_tags: Vec<TagResponse>,
|
||||
all_tags: Vec<TagResponse>,
|
||||
server_url: String,
|
||||
#[props(default = false)] autoplay: bool,
|
||||
on_back: EventHandler<()>,
|
||||
on_open: EventHandler<String>,
|
||||
on_update: EventHandler<MediaUpdateEvent>,
|
||||
on_tag: EventHandler<(String, String)>,
|
||||
on_untag: EventHandler<(String, String)>,
|
||||
on_set_custom_field: EventHandler<(String, String, String, String)>,
|
||||
on_delete_custom_field: EventHandler<(String, String)>,
|
||||
on_delete: EventHandler<String>,
|
||||
) -> Element {
|
||||
let mut editing = use_signal(|| false);
|
||||
let mut show_image_viewer = use_signal(|| false);
|
||||
let mut edit_title = use_signal(String::new);
|
||||
let mut edit_artist = use_signal(String::new);
|
||||
let mut edit_album = use_signal(String::new);
|
||||
let mut edit_genre = use_signal(String::new);
|
||||
let mut edit_year = use_signal(String::new);
|
||||
let mut edit_description = use_signal(String::new);
|
||||
|
||||
let mut add_tag_id = use_signal(String::new);
|
||||
|
||||
let mut new_field_name = use_signal(String::new);
|
||||
let mut new_field_type = use_signal(|| "text".to_string());
|
||||
let mut new_field_value = use_signal(String::new);
|
||||
|
||||
let mut confirm_delete = use_signal(|| false);
|
||||
|
||||
let id = media.id.clone();
|
||||
let title = media.title.clone().unwrap_or_default();
|
||||
let artist = media.artist.clone().unwrap_or_default();
|
||||
let album = media.album.clone().unwrap_or_default();
|
||||
let genre = media.genre.clone().unwrap_or_default();
|
||||
let year_str = media.year.map(|y| y.to_string()).unwrap_or_default();
|
||||
let duration_str = media.duration_secs.map(format_duration).unwrap_or_default();
|
||||
let description = media.description.clone().unwrap_or_default();
|
||||
let size = format_size(media.file_size);
|
||||
let badge_class = type_badge_class(&media.media_type);
|
||||
let custom_fields: Vec<(String, String, String)> = media
|
||||
.custom_fields
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.field_type.clone(), v.value.clone()))
|
||||
.collect();
|
||||
|
||||
let is_editing = editing();
|
||||
|
||||
// Separate system-extracted metadata from user-defined custom fields.
|
||||
// System fields are those set by extractors (camera info, dimensions, etc.)
|
||||
let system_field_names: &[&str] = &[
|
||||
"width",
|
||||
"height",
|
||||
"camera_make",
|
||||
"camera_model",
|
||||
"date_taken",
|
||||
"gps_latitude",
|
||||
"gps_longitude",
|
||||
"iso",
|
||||
"exposure_time",
|
||||
"f_number",
|
||||
"focal_length",
|
||||
"software",
|
||||
"lens_model",
|
||||
"flash",
|
||||
"orientation",
|
||||
"track_number",
|
||||
"disc_number",
|
||||
"comment",
|
||||
"bitrate",
|
||||
"sample_rate",
|
||||
"channels",
|
||||
"resolution",
|
||||
"video_codec",
|
||||
"audio_codec",
|
||||
"audio_bitrate",
|
||||
];
|
||||
let system_fields: Vec<(String, String, String)> = custom_fields
|
||||
.iter()
|
||||
.filter(|(k, _, _)| system_field_names.contains(&k.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
let user_fields: Vec<(String, String, String)> = custom_fields
|
||||
.iter()
|
||||
.filter(|(k, _, _)| !system_field_names.contains(&k.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
let has_system_fields = !system_fields.is_empty();
|
||||
let has_user_fields = !user_fields.is_empty();
|
||||
|
||||
// Media preview URLs
|
||||
let stream_url = format!("{}/api/v1/media/{}/stream", server_url, media.id);
|
||||
let thumbnail_url = format!("{}/api/v1/media/{}/thumbnail", server_url, media.id);
|
||||
let category = media_category(&media.media_type);
|
||||
let has_thumbnail = media.has_thumbnail;
|
||||
|
||||
// Compute available tags (all_tags minus media_tags)
|
||||
let media_tag_ids: Vec<String> = media_tags.iter().map(|t| t.id.clone()).collect();
|
||||
let available_tags: Vec<TagResponse> = all_tags
|
||||
.iter()
|
||||
.filter(|t| !media_tag_ids.contains(&t.id))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Clone values needed for closures
|
||||
let id_for_open = id.clone();
|
||||
let id_for_save = id.clone();
|
||||
let id_for_tag = id.clone();
|
||||
let id_for_field = id.clone();
|
||||
let id_for_delete = id.clone();
|
||||
|
||||
// Clone media field values for the edit button
|
||||
let title_for_edit = media.title.clone().unwrap_or_default();
|
||||
let artist_for_edit = media.artist.clone().unwrap_or_default();
|
||||
let album_for_edit = media.album.clone().unwrap_or_default();
|
||||
let genre_for_edit = media.genre.clone().unwrap_or_default();
|
||||
let year_for_edit = media.year.map(|y| y.to_string()).unwrap_or_default();
|
||||
let description_for_edit = media.description.clone().unwrap_or_default();
|
||||
|
||||
let on_edit_click = move |_| {
|
||||
edit_title.set(title_for_edit.clone());
|
||||
edit_artist.set(artist_for_edit.clone());
|
||||
edit_album.set(album_for_edit.clone());
|
||||
edit_genre.set(genre_for_edit.clone());
|
||||
edit_year.set(year_for_edit.clone());
|
||||
edit_description.set(description_for_edit.clone());
|
||||
editing.set(true);
|
||||
};
|
||||
|
||||
let on_save_click = {
|
||||
let id_save = id_for_save.clone();
|
||||
move |_| {
|
||||
let t = edit_title();
|
||||
let ar = edit_artist();
|
||||
let al = edit_album();
|
||||
let g = edit_genre();
|
||||
let y_str = edit_year();
|
||||
let d = edit_description();
|
||||
|
||||
let title_opt = if t.is_empty() { None } else { Some(t) };
|
||||
let artist_opt = if ar.is_empty() { None } else { Some(ar) };
|
||||
let album_opt = if al.is_empty() { None } else { Some(al) };
|
||||
let genre_opt = if g.is_empty() { None } else { Some(g) };
|
||||
let year_opt = if y_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
y_str.parse::<i32>().ok()
|
||||
};
|
||||
let desc_opt = if d.is_empty() { None } else { Some(d) };
|
||||
|
||||
on_update.call(MediaUpdateEvent {
|
||||
id: id_save.clone(),
|
||||
title: title_opt,
|
||||
artist: artist_opt,
|
||||
album: album_opt,
|
||||
genre: genre_opt,
|
||||
year: year_opt,
|
||||
description: desc_opt,
|
||||
});
|
||||
editing.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
let on_cancel_click = move |_| {
|
||||
editing.set(false);
|
||||
};
|
||||
|
||||
let on_tag_add_click = {
|
||||
let id_tag = id_for_tag.clone();
|
||||
move |_| {
|
||||
let tag_id = add_tag_id();
|
||||
if !tag_id.is_empty() {
|
||||
on_tag.call((id_tag.clone(), tag_id));
|
||||
add_tag_id.set(String::new());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let on_add_field_click = {
|
||||
let id_field = id_for_field.clone();
|
||||
move |_| {
|
||||
let name = new_field_name();
|
||||
let ft = new_field_type();
|
||||
let val = new_field_value();
|
||||
if !name.is_empty() && !val.is_empty() {
|
||||
on_set_custom_field.call((id_field.clone(), name, ft, val));
|
||||
new_field_name.set(String::new());
|
||||
new_field_type.set("text".to_string());
|
||||
new_field_value.set(String::new());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let on_delete_click = move |_| {
|
||||
confirm_delete.set(true);
|
||||
};
|
||||
|
||||
let on_confirm_delete = {
|
||||
let id_del = id_for_delete.clone();
|
||||
move |_| {
|
||||
on_delete.call(id_del.clone());
|
||||
confirm_delete.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
let on_cancel_delete = move |_| {
|
||||
confirm_delete.set(false);
|
||||
};
|
||||
|
||||
let stream_url_for_viewer = stream_url.clone();
|
||||
let thumb_for_player = thumbnail_url.clone();
|
||||
let file_name_for_viewer = media.file_name.clone();
|
||||
|
||||
rsx! {
|
||||
// Media preview
|
||||
div { class: "detail-preview",
|
||||
if category == "audio" {
|
||||
MediaPlayer {
|
||||
src: stream_url.clone(),
|
||||
media_type: "audio".to_string(),
|
||||
title: media.title.clone(),
|
||||
thumbnail_url: if has_thumbnail { Some(thumb_for_player.clone()) } else { None },
|
||||
autoplay: autoplay,
|
||||
}
|
||||
} else if category == "video" {
|
||||
MediaPlayer {
|
||||
src: stream_url.clone(),
|
||||
media_type: "video".to_string(),
|
||||
title: media.title.clone(),
|
||||
autoplay: autoplay,
|
||||
}
|
||||
} else if category == "image" {
|
||||
if has_thumbnail {
|
||||
img {
|
||||
src: "{thumbnail_url}",
|
||||
alt: "{media.file_name}",
|
||||
class: "detail-preview-image clickable",
|
||||
onclick: move |_| show_image_viewer.set(true),
|
||||
}
|
||||
} else {
|
||||
img {
|
||||
src: "{stream_url}",
|
||||
alt: "{media.file_name}",
|
||||
class: "detail-preview-image clickable",
|
||||
onclick: move |_| show_image_viewer.set(true),
|
||||
}
|
||||
}
|
||||
} else if category == "text" {
|
||||
MarkdownViewer {
|
||||
content_url: stream_url.clone(),
|
||||
media_type: media.media_type.clone(),
|
||||
}
|
||||
} else if category == "document" {
|
||||
div { class: "detail-no-preview",
|
||||
p { class: "text-muted", "Preview not available for this document type." }
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
onclick: {
|
||||
let id_open = id.clone();
|
||||
move |_| on_open.call(id_open.clone())
|
||||
},
|
||||
"Open Externally"
|
||||
}
|
||||
}
|
||||
} else if has_thumbnail {
|
||||
img {
|
||||
src: "{thumbnail_url}",
|
||||
alt: "Thumbnail",
|
||||
class: "detail-thumbnail",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action bar
|
||||
div { class: "detail-actions",
|
||||
button {
|
||||
class: "btn btn-secondary",
|
||||
onclick: move |_| on_back.call(()),
|
||||
"Back"
|
||||
}
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
onclick: {
|
||||
let id_open = id_for_open.clone();
|
||||
move |_| on_open.call(id_open.clone())
|
||||
},
|
||||
"Open"
|
||||
}
|
||||
if is_editing {
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
onclick: on_save_click,
|
||||
"Save"
|
||||
}
|
||||
button {
|
||||
class: "btn btn-ghost",
|
||||
onclick: on_cancel_click,
|
||||
"Cancel"
|
||||
}
|
||||
} else {
|
||||
button {
|
||||
class: "btn btn-secondary",
|
||||
onclick: on_edit_click,
|
||||
"Edit"
|
||||
}
|
||||
}
|
||||
if confirm_delete() {
|
||||
button {
|
||||
class: "btn btn-danger",
|
||||
onclick: on_confirm_delete,
|
||||
"Confirm Delete"
|
||||
}
|
||||
button {
|
||||
class: "btn btn-ghost",
|
||||
onclick: on_cancel_delete,
|
||||
"Cancel"
|
||||
}
|
||||
} else {
|
||||
button {
|
||||
class: "btn btn-danger",
|
||||
onclick: on_delete_click,
|
||||
"Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info / Edit section
|
||||
if is_editing {
|
||||
div { class: "detail-grid",
|
||||
// Read-only file info
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "File Name" }
|
||||
span { class: "detail-value", "{media.file_name}" }
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Path" }
|
||||
span { class: "detail-value mono", "{media.path}" }
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Type" }
|
||||
span { class: "detail-value",
|
||||
span { class: "type-badge {badge_class}", "{media.media_type}" }
|
||||
}
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Size" }
|
||||
span { class: "detail-value", "{size}" }
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Hash" }
|
||||
span { class: "detail-value mono", "{media.content_hash}" }
|
||||
}
|
||||
|
||||
// Editable fields — conditional by media category
|
||||
div { class: "detail-field",
|
||||
label { class: "detail-label", "Title" }
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{edit_title}",
|
||||
oninput: move |e: Event<FormData>| edit_title.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "detail-field",
|
||||
label { class: "detail-label",
|
||||
{match category {
|
||||
"image" => "Photographer",
|
||||
"document" | "text" => "Author",
|
||||
_ => "Artist",
|
||||
}}
|
||||
}
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{edit_artist}",
|
||||
oninput: move |e: Event<FormData>| edit_artist.set(e.value()),
|
||||
}
|
||||
}
|
||||
if category == "audio" {
|
||||
div { class: "detail-field",
|
||||
label { class: "detail-label", "Album" }
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{edit_album}",
|
||||
oninput: move |e: Event<FormData>| edit_album.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
if category == "audio" || category == "video" {
|
||||
div { class: "detail-field",
|
||||
label { class: "detail-label", "Genre" }
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{edit_genre}",
|
||||
oninput: move |e: Event<FormData>| edit_genre.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
if category == "audio" || category == "video" || category == "document" {
|
||||
div { class: "detail-field",
|
||||
label { class: "detail-label", "Year" }
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{edit_year}",
|
||||
oninput: move |e: Event<FormData>| edit_year.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
div { class: "detail-field full-width",
|
||||
label { class: "detail-label", "Description" }
|
||||
textarea {
|
||||
value: "{edit_description}",
|
||||
oninput: move |e: Event<FormData>| edit_description.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
div { class: "detail-grid",
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "File Name" }
|
||||
span { class: "detail-value", "{media.file_name}" }
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Path" }
|
||||
span { class: "detail-value mono", "{media.path}" }
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Type" }
|
||||
span { class: "detail-value",
|
||||
span { class: "type-badge {badge_class}", "{media.media_type}" }
|
||||
}
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Size" }
|
||||
span { class: "detail-value", "{size}" }
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Hash" }
|
||||
span { class: "detail-value mono", "{media.content_hash}" }
|
||||
}
|
||||
// Title: only shown when non-empty
|
||||
if !title.is_empty() {
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Title" }
|
||||
span { class: "detail-value", "{title}" }
|
||||
}
|
||||
}
|
||||
// Artist/Author/Photographer: only shown when non-empty
|
||||
if !artist.is_empty() {
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label",
|
||||
{match category {
|
||||
"image" => "Photographer",
|
||||
"document" | "text" => "Author",
|
||||
_ => "Artist",
|
||||
}}
|
||||
}
|
||||
span { class: "detail-value", "{artist}" }
|
||||
}
|
||||
}
|
||||
// Album: audio only, when non-empty
|
||||
if category == "audio" && !album.is_empty() {
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Album" }
|
||||
span { class: "detail-value", "{album}" }
|
||||
}
|
||||
}
|
||||
// Genre: audio and video, when non-empty
|
||||
if (category == "audio" || category == "video") && !genre.is_empty() {
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Genre" }
|
||||
span { class: "detail-value", "{genre}" }
|
||||
}
|
||||
}
|
||||
// Year: audio, video, document, when non-empty
|
||||
if (category == "audio" || category == "video" || category == "document") && !year_str.is_empty() {
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Year" }
|
||||
span { class: "detail-value", "{year_str}" }
|
||||
}
|
||||
}
|
||||
// Duration: audio and video
|
||||
if (category == "audio" || category == "video") && media.duration_secs.is_some() {
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Duration" }
|
||||
span { class: "detail-value", "{duration_str}" }
|
||||
}
|
||||
}
|
||||
// Description: only shown when non-empty
|
||||
if !description.is_empty() {
|
||||
div { class: "detail-field full-width",
|
||||
span { class: "detail-label", "Description" }
|
||||
span { class: "detail-value", "{description}" }
|
||||
}
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Created" }
|
||||
span { class: "detail-value", "{media.created_at}" }
|
||||
}
|
||||
div { class: "detail-field",
|
||||
span { class: "detail-label", "Updated" }
|
||||
span { class: "detail-value", "{media.updated_at}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tags section
|
||||
div { class: "card mb-16",
|
||||
div { class: "card-header",
|
||||
h4 { class: "card-title", "Tags" }
|
||||
}
|
||||
div { class: "tag-list mb-8",
|
||||
for tag in media_tags.iter() {
|
||||
{
|
||||
let tag_id = tag.id.clone();
|
||||
let media_id_untag = id.clone();
|
||||
rsx! {
|
||||
span {
|
||||
class: "tag-badge",
|
||||
key: "{tag_id}",
|
||||
"{tag.name}"
|
||||
span {
|
||||
class: "tag-remove",
|
||||
onclick: {
|
||||
let tid = tag_id.clone();
|
||||
let mid = media_id_untag.clone();
|
||||
move |_| on_untag.call((mid.clone(), tid.clone()))
|
||||
},
|
||||
"x"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div { class: "form-row",
|
||||
select {
|
||||
value: "{add_tag_id}",
|
||||
onchange: move |e: Event<FormData>| add_tag_id.set(e.value()),
|
||||
option { value: "", "Add tag..." }
|
||||
for tag in available_tags.iter() {
|
||||
{
|
||||
let tid = tag.id.clone();
|
||||
let tname = tag.name.clone();
|
||||
rsx! {
|
||||
option {
|
||||
key: "{tid}",
|
||||
value: "{tid}",
|
||||
"{tname}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "btn btn-sm btn-primary",
|
||||
onclick: on_tag_add_click,
|
||||
"Add"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Technical Metadata section (system-extracted fields)
|
||||
if has_system_fields {
|
||||
div { class: "card mb-16",
|
||||
div { class: "card-header",
|
||||
h4 { class: "card-title", "Technical Metadata" }
|
||||
}
|
||||
div { class: "detail-grid",
|
||||
for (key, _field_type, value) in system_fields.iter() {
|
||||
div {
|
||||
class: "detail-field",
|
||||
key: "{key}",
|
||||
span { class: "detail-label", "{key}" }
|
||||
span { class: "detail-value", "{value}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Fields section (user-defined)
|
||||
div { class: "card",
|
||||
div { class: "card-header",
|
||||
h4 { class: "card-title", "Custom Fields" }
|
||||
}
|
||||
if has_user_fields {
|
||||
div { class: "detail-grid",
|
||||
for (key, field_type, value) in user_fields.iter() {
|
||||
{
|
||||
let field_name = key.clone();
|
||||
let media_id_del = id.clone();
|
||||
rsx! {
|
||||
div {
|
||||
class: "detail-field",
|
||||
key: "{field_name}",
|
||||
span { class: "detail-label", "{key} ({field_type})" }
|
||||
div { class: "flex-row",
|
||||
span { class: "detail-value", "{value}" }
|
||||
button {
|
||||
class: "btn-icon",
|
||||
onclick: {
|
||||
let fname = field_name.clone();
|
||||
let mid = media_id_del.clone();
|
||||
move |_| on_delete_custom_field.call((mid.clone(), fname.clone()))
|
||||
},
|
||||
"x"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div { class: "form-row",
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "Field name",
|
||||
value: "{new_field_name}",
|
||||
oninput: move |e: Event<FormData>| new_field_name.set(e.value()),
|
||||
}
|
||||
select {
|
||||
value: "{new_field_type}",
|
||||
onchange: move |e: Event<FormData>| new_field_type.set(e.value()),
|
||||
option { value: "text", "text" }
|
||||
option { value: "number", "number" }
|
||||
option { value: "date", "date" }
|
||||
option { value: "boolean", "boolean" }
|
||||
}
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "Value",
|
||||
value: "{new_field_value}",
|
||||
oninput: move |e: Event<FormData>| new_field_value.set(e.value()),
|
||||
}
|
||||
button {
|
||||
class: "btn btn-sm btn-primary",
|
||||
onclick: on_add_field_click,
|
||||
"Add"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Image viewer overlay
|
||||
if *show_image_viewer.read() {
|
||||
ImageViewer {
|
||||
src: stream_url_for_viewer.clone(),
|
||||
alt: file_name_for_viewer.clone(),
|
||||
on_close: move |_| show_image_viewer.set(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue