From 280bdcda8fa0640f2f178e7aa2309da227af8e31 Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 3 Jul 2025 20:17:00 +0200 Subject: [PATCH] web ui --- frontend/src/scss/_default.scss | 2 +- shared/src/foundation/filter.rs | 11 +- shared/src/model/config/rename.rs | 2 +- shared/src/model/config/sort.rs | 30 +++-- shared/src/model/config/target.rs | 14 +-- shared/src/model/config/trakt.rs | 6 +- webui/public/assets/i18n/en.json | 24 +++- webui/public/assets/icons.json | 24 ++++ webui/scss/_action_card.scss | 36 ++++++ webui/scss/_app.scss | 6 +- webui/scss/_app_header.scss | 5 +- webui/scss/_breadcrumbs.scss | 53 +++++++++ webui/scss/_card.scss | 2 +- webui/scss/_collapse_panel.scss | 2 +- webui/scss/_common.scss | 105 +----------------- webui/scss/_component.scss | 7 ++ webui/scss/_dashboard_view.scss | 59 +--------- webui/scss/_default.scss | 48 +------- webui/scss/_icon_button.scss | 2 +- webui/scss/_input.scss | 2 +- webui/scss/_login.scss | 2 +- webui/scss/_menu_item.scss | 2 +- webui/scss/_panel.scss | 8 +- webui/scss/_playlist_assistant.scss | 3 + webui/scss/_playlist_view.scss | 39 +++++++ webui/scss/_preferences.scss | 72 +----------- webui/scss/_sidebar.scss | 2 +- webui/scss/_status_card.scss | 15 +++ webui/scss/_table.scss | 33 ++++++ webui/scss/_text_button.scss | 6 +- webui/scss/_theme.scss | 14 ++- webui/scss/_toggle_switch.scss | 44 ++++++++ webui/src/app/components/breadcrumbs.rs | 65 +++++++++++ webui/src/app/components/card.rs | 2 +- webui/src/app/components/collapse_panel.rs | 6 +- .../app/components/dashboard/action_card.rs | 10 +- .../dashboard/discord_action_card.rs | 2 +- .../dashboard/documentation_action_card.rs | 2 +- .../dashboard/ipinfo_action_card.rs | 4 +- .../app/components/dashboard/status_card.rs | 9 +- webui/src/app/components/dashboard_view.rs | 10 +- webui/src/app/components/home.rs | 15 +-- webui/src/app/components/icon_button.rs | 2 +- webui/src/app/components/input.rs | 4 +- webui/src/app/components/login.rs | 16 +-- webui/src/app/components/menu_item.rs | 2 +- webui/src/app/components/mod.rs | 11 +- webui/src/app/components/panel.rs | 2 +- .../app/components/playlist/assistant/mod.rs | 12 ++ .../components/playlist/assistant/playlist.rs | 61 ++++++++++ .../assistant/playlist_assistant_context.rs | 6 + .../playlist/assistant/step_name.rs | 12 ++ .../playlist/assistant/step_schedule.rs | 12 ++ .../playlist/assistant/step_type.rs | 12 ++ webui/src/app/components/playlist/context.rs | 7 ++ webui/src/app/components/playlist/create.rs | 33 ++++++ webui/src/app/components/playlist/list.rs | 33 ++++++ webui/src/app/components/playlist/mod.rs | 13 +++ webui/src/app/components/playlist/page.rs | 31 ++++++ .../app/components/playlist/target_table.rs | 79 +++++++++++++ webui/src/app/components/playlist_view.rs | 53 +++++++++ webui/src/app/components/preferences.rs | 20 ++-- webui/src/app/components/sidebar.rs | 16 ++- webui/src/app/components/svg_icon.rs | 2 +- webui/src/app/components/table.rs | 63 +++++++++++ webui/src/app/components/text_button.rs | 2 +- webui/src/app/components/toggle_switch.rs | 29 +++++ webui/src/app/mod.rs | 1 - webui/src/model/view_type.rs | 3 + webui/src/provider/icon_context_provider.rs | 1 - .../src/provider/service_context_provider.rs | 1 - 71 files changed, 954 insertions(+), 390 deletions(-) create mode 100644 webui/scss/_action_card.scss create mode 100644 webui/scss/_breadcrumbs.scss create mode 100644 webui/scss/_playlist_assistant.scss create mode 100644 webui/scss/_playlist_view.scss create mode 100644 webui/scss/_status_card.scss create mode 100644 webui/scss/_table.scss create mode 100644 webui/scss/_toggle_switch.scss create mode 100644 webui/src/app/components/breadcrumbs.rs create mode 100644 webui/src/app/components/playlist/assistant/mod.rs create mode 100644 webui/src/app/components/playlist/assistant/playlist.rs create mode 100644 webui/src/app/components/playlist/assistant/playlist_assistant_context.rs create mode 100644 webui/src/app/components/playlist/assistant/step_name.rs create mode 100644 webui/src/app/components/playlist/assistant/step_schedule.rs create mode 100644 webui/src/app/components/playlist/assistant/step_type.rs create mode 100644 webui/src/app/components/playlist/context.rs create mode 100644 webui/src/app/components/playlist/create.rs create mode 100644 webui/src/app/components/playlist/list.rs create mode 100644 webui/src/app/components/playlist/mod.rs create mode 100644 webui/src/app/components/playlist/page.rs create mode 100644 webui/src/app/components/playlist/target_table.rs create mode 100644 webui/src/app/components/playlist_view.rs create mode 100644 webui/src/app/components/table.rs create mode 100644 webui/src/app/components/toggle_switch.rs diff --git a/frontend/src/scss/_default.scss b/frontend/src/scss/_default.scss index f0b2117d6..74fe1d26e 100644 --- a/frontend/src/scss/_default.scss +++ b/frontend/src/scss/_default.scss @@ -25,7 +25,7 @@ button { //} } -.hidden { +.tp__hidden { display: none !important; } diff --git a/shared/src/foundation/filter.rs b/shared/src/foundation/filter.rs index b4cb6efd2..6f79d2039 100644 --- a/shared/src/foundation/filter.rs +++ b/shared/src/foundation/filter.rs @@ -78,6 +78,11 @@ pub struct CompiledRegex { pub restr: String, pub re: regex::Regex, } +impl PartialEq for CompiledRegex { + fn eq(&self, other: &Self) -> bool { + self.restr == other.restr + } +} #[derive(Parser)] #[grammar_inline = r#" @@ -105,12 +110,12 @@ main = _{ SOI ~ stmt ~ EOI } "#] struct FilterParser; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum UnaryOperator { Not, } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum BinaryOperator { And, Or, @@ -130,7 +135,7 @@ impl std::fmt::Display for BinaryOperator { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Filter { Group(Box), FieldComparison(ItemField, CompiledRegex), diff --git a/shared/src/model/config/rename.rs b/shared/src/model/config/rename.rs index 8560ba6ab..426b2a2df 100644 --- a/shared/src/model/config/rename.rs +++ b/shared/src/model/config/rename.rs @@ -2,7 +2,7 @@ use crate::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKin use crate::foundation::filter::apply_templates_to_pattern_single; use crate::model::{ItemField, PatternTemplate}; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct ConfigRenameDto { pub field: ItemField, diff --git a/shared/src/model/config/sort.rs b/shared/src/model/config/sort.rs index d9330069f..a41dedf37 100644 --- a/shared/src/model/config/sort.rs +++ b/shared/src/model/config/sort.rs @@ -1,8 +1,8 @@ -use regex::Regex; -use crate::{create_tuliprox_error, handle_tuliprox_error_result_list}; use crate::error::{TuliproxError, TuliproxErrorKind}; use crate::foundation::filter::{apply_templates_to_pattern, apply_templates_to_pattern_single}; use crate::model::{ItemField, PatternTemplate, TemplateValue}; +use crate::{create_tuliprox_error, handle_tuliprox_error_result_list}; +use regex::Regex; fn compile_regex_vec(patterns: Option<&Vec>) -> Result>, TuliproxError> { patterns.as_ref() @@ -16,7 +16,7 @@ fn compile_regex_vec(patterns: Option<&Vec>) -> Result .transpose() // convert Option> to Result> } -#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub enum SortOrder { #[serde(rename = "asc")] Asc, @@ -34,8 +34,14 @@ pub struct ConfigSortGroupDto { pub t_sequence: Option>, } -impl ConfigSortGroupDto { +impl PartialEq for ConfigSortGroupDto { + fn eq(&self, other: &Self) -> bool { + self.order == other.order + && self.sequence == other.sequence + } +} +impl ConfigSortGroupDto { pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliproxError> { let processed_sequence = match (&self.sequence, templates) { (Some(seqs), Some(_templs)) => { @@ -47,7 +53,7 @@ impl ConfigSortGroupDto { } } Some(result) - }, + } (Some(seqs), None) => Some(seqs.clone()), (None, _) => None, }; @@ -71,6 +77,16 @@ pub struct ConfigSortChannelDto { pub t_sequence: Option>, } +impl PartialEq for ConfigSortChannelDto { + fn eq(&self, other: &Self) -> bool { + self.field == other.field + && self.group_pattern == other.group_pattern + && self.order == other.order + && self.sequence == other.sequence + } +} + + impl ConfigSortChannelDto { pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliproxError> { self.group_pattern = apply_templates_to_pattern_single(&self.group_pattern, templates)?; @@ -90,7 +106,7 @@ impl ConfigSortChannelDto { } } Some(result) - }, + } (Some(seqs), None) => Some(seqs.clone()), (None, _) => None, }; @@ -101,7 +117,7 @@ impl ConfigSortChannelDto { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)] #[serde(deny_unknown_fields)] pub struct ConfigSortDto { #[serde(default)] diff --git a/shared/src/model/config/target.rs b/shared/src/model/config/target.rs index 0c035740d..acf8d4f9a 100644 --- a/shared/src/model/config/target.rs +++ b/shared/src/model/config/target.rs @@ -3,7 +3,7 @@ use crate::{create_tuliprox_error_result, handle_tuliprox_error_result_list, inf use crate::foundation::filter::{get_filter, Filter}; use crate::model::{ClusterFlags, ConfigRenameDto, ConfigSortDto, PatternTemplate, ProcessingOrder, StrmExportStyle, TargetType, TraktConfigDto}; use crate::utils::{default_as_true, default_resolve_delay_secs, default_as_default}; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)] #[serde(deny_unknown_fields)] pub struct ConfigTargetOptions { #[serde(default)] @@ -17,7 +17,7 @@ pub struct ConfigTargetOptions { } #[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct XtreamTargetOutputDto { #[serde(default = "default_as_true")] @@ -46,7 +46,7 @@ impl XtreamTargetOutputDto { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct M3uTargetOutputDto { #[serde(skip_serializing_if = "Option::is_none")] @@ -57,7 +57,7 @@ pub struct M3uTargetOutputDto { pub mask_redirect_url: bool, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct StrmTargetOutputDto { pub directory: String, @@ -74,7 +74,7 @@ pub struct StrmTargetOutputDto { #[serde(default, skip_serializing_if = "Option::is_none")] pub strm_props: Option>, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct HdHomeRunTargetOutputDto { pub device: String, @@ -83,7 +83,7 @@ pub struct HdHomeRunTargetOutputDto { pub use_output: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")] pub enum TargetOutputDto { Xtream(XtreamTargetOutputDto), @@ -103,7 +103,7 @@ impl TargetOutputDto { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)] #[serde(deny_unknown_fields)] pub struct ConfigTargetDto { #[serde(skip)] diff --git a/shared/src/model/config/trakt.rs b/shared/src/model/config/trakt.rs index 026e32288..0e0e05abe 100644 --- a/shared/src/model/config/trakt.rs +++ b/shared/src/model/config/trakt.rs @@ -21,7 +21,7 @@ impl Default for TraktContentType { } } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct TraktApiConfigDto { #[serde(default)] @@ -44,7 +44,7 @@ impl TraktApiConfigDto { } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct TraktListConfigDto { pub user: String, @@ -55,7 +55,7 @@ pub struct TraktListConfigDto { pub fuzzy_match_threshold: u8, // Percentage (0-100) } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct TraktConfigDto { #[serde(default)] diff --git a/webui/public/assets/i18n/en.json b/webui/public/assets/i18n/en.json index 6768cc7d4..f367a215e 100644 --- a/webui/public/assets/i18n/en.json +++ b/webui/public/assets/i18n/en.json @@ -107,8 +107,8 @@ "BUILD_TIME": "Build time", "MEMORY": "Memory", "ACTIVE_USERS": "Active users", - "ACTIVE_USER_CONNECTIONS": "Active user conn...", - "ACTIVE_PROVIDER_CONNECTIONS": "Active provider conn...", + "ACTIVE_USER_CONNECTIONS": "Active user connections", + "ACTIVE_PROVIDER_CONNECTIONS": "Active provider connections", "IP_CHECK": "IP Check", "URL_IPV4": "Url IPv4", "URL_IPV6": "Url IPv6", @@ -130,7 +130,25 @@ "DOCUMENTATION": "Documentation", "DOCUMENTATION_CONTENT": "Get started with tuliprox: features, setup, and examples", "OPEN_DOCUMENTATION": "Documentation", - "NOT_ACTIVATED": "Feature is not activated" + "NOT_ACTIVATED": "Feature is not activated", + "CREATE": "Create", + "PLAYLIST": "Playlist", + "PLAYLISTS": "Playlists", + "LIST": "List", + "NEW_PLAYLIST": "New Playlist" + }, + "TABLE": { + "ID": "Id", + "ENABLED": "Enabled", + "NAME": "Name", + "OPTIONS": "Options", + "SORT": "Sort", + "FILTER": "Filter", + "OUTPUT": "Output", + "RENAME": "Rename", + "MAPPING": "Mapping", + "PROCESSING_ORDER": "Processing order", + "WATCH": "Watch" }, "TITLE": { "USER_BOUQUET_EDITOR": "User group editor" diff --git a/webui/public/assets/icons.json b/webui/public/assets/icons.json index cb7de1352..3efdb6387 100644 --- a/webui/public/assets/icons.json +++ b/webui/public/assets/icons.json @@ -89,6 +89,12 @@ ], "path": "M8 5v14l11-7z" }, + { + "keys": [ + "PlayArrowOutline" + ], + "path": "m 6.9091055,7.1999977 7.6654235,4.7999983 -7.6654235,4.8 z M 4,2 V 22 L 20,11.999996 Z" + }, { "keys": [ "ArrowLeft" @@ -185,6 +191,12 @@ ], "path": "M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" }, + { + "keys": [ + "UserOutline" + ], + "path": "m 12,4.5 c 1.375,0 2.5,1.125 2.5,2.5 0,1.375 -1.125,2.5 -2.5,2.5 C 10.625,9.5 9.5,8.375 9.5,7 9.5,5.625 10.625,4.5 12,4.5 M 12,17 c 3.375,0 7.25,1.6125 7.5,2.5 H 4.5 C 4.7875,18.6 8.6375,17 12,17 M 12,2 C 9.2375,2 7,4.2375 7,7 c 0,2.7625 2.2375,5 5,5 2.7625,0 5,-2.2375 5,-5 0,-2.7625 -2.2375,-5 -5,-5 m 0,12.5 c -3.3375,0 -10,1.675 -10,5 V 22 h 20 v -2.5 c 0,-3.325 -6.6625,-5 -10,-5" + }, { "keys": [ "Refresh" @@ -299,6 +311,10 @@ "keys": ["Dashboard"], "path": "M11 21H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h6zm2 0h6c1.1 0 2-.9 2-2v-7h-8zm8-11V5c0-1.1-.9-2-2-2h-6v7z" }, + { + "keys": ["DashboardOutline"], + "path": "M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M5 19V5h6v14zm14 0h-6v-7h6zm0-9h-6V5h6z" + }, { "keys": ["Settings"], "path": "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6" @@ -318,5 +334,13 @@ { "keys": ["Router", "Network"], "path": "m20.2 5.9.8-.8C19.6 3.7 17.8 3 16 3s-3.6.7-5 2.1l.8.8C13 4.8 14.5 4.2 16 4.2s3 .6 4.2 1.7m-.9.8c-.9-.9-2.1-1.4-3.3-1.4s-2.4.5-3.3 1.4l.8.8c.7-.7 1.6-1 2.5-1s1.8.3 2.5 1zM19 13h-2V9h-2v4H5c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2M8 18H6v-2h2zm3.5 0h-2v-2h2zm3.5 0h-2v-2h2z" + }, + { + "keys": ["PlaylistAdd"], + "path": "M 13.578946,8.2857137 H 2 V 10.428572 H 13.578946 Z M 13.578946,4 H 2 v 2.1428548 h 11.578946 z m 4.210508,8.571428 V 8.2857137 h -2.105235 v 4.2857143 h -4.210547 v 2.142859 h 4.210547 V 19 h 2.105235 V 14.714287 H 22 V 12.571428 Z M 2,14.714287 H 9.368438 V 12.571428 H 2 Z" + }, + { + "keys": ["Playlist"], + "path": "M 2,8.2666668 H 13.578948 V 10.4 H 2 Z M 2,4 H 13.578948 V 6.1333336 H 2 Z m 0,8.533334 h 7.3684211 v 2.133333 H 2 Z M 15.684211,11.466667 V 20 L 22,15.733334 Z" } ] \ No newline at end of file diff --git a/webui/scss/_action_card.scss b/webui/scss/_action_card.scss new file mode 100644 index 000000000..2113b168c --- /dev/null +++ b/webui/scss/_action_card.scss @@ -0,0 +1,36 @@ +.tp__action-card { + display: flex; + flex-flow: row nowrap; + align-content: flex-start; + flex: 1 1 auto; + gap: var(--gap-default); + fill: var(--card-color); + + &__icon { + fill: inherit; + width: 3rem; + transform: translateY(-0.5rem); + + img { + width: 3rem; + } + } + + &__body { + display: flex; + flex-flow: column; + align-content: flex-start; + flex: 1 1 auto; + gap: var(--gap-small); + } + + &__title { + color: var(--text-color); + font-weight: bold; + font-size: 1.1rem; + } + + &__content { + + } +} \ No newline at end of file diff --git a/webui/scss/_app.scss b/webui/scss/_app.scss index dedddc005..3ed524109 100644 --- a/webui/scss/_app.scss +++ b/webui/scss/_app.scss @@ -1,6 +1,4 @@ -@use 'common'; - -.app { +.tp__app { display: flex; flex-flow: row; overflow: hidden; @@ -16,6 +14,8 @@ flex: 1 1 auto; align-content: flex-start; justify-content: flex-start; + overflow: hidden; + &__body { display: flex; flex-flow: column; diff --git a/webui/scss/_app_header.scss b/webui/scss/_app_header.scss index 36050e468..1db63209d 100644 --- a/webui/scss/_app_header.scss +++ b/webui/scss/_app_header.scss @@ -1,5 +1,4 @@ - -.app-header { +.tp__app-header { display: flex; align-items: center; justify-content: space-between; @@ -16,7 +15,7 @@ display: flex; align-items: center; gap: var(--gap-small); - .icon-button { + .tp__icon-button { width: 2.5rem; } } diff --git a/webui/scss/_breadcrumbs.scss b/webui/scss/_breadcrumbs.scss new file mode 100644 index 000000000..80cfef231 --- /dev/null +++ b/webui/scss/_breadcrumbs.scss @@ -0,0 +1,53 @@ +.tp__breadcrumbs { + display: flex; + flex-flow: row; + align-items: center; + gap: 4px; + border-radius: 5px; + box-sizing: border-box; + + ol { + display: flex; + flex-flow: row; + list-style-type: none; + padding: 0; + margin: 0; + align-items: center; + box-sizing: border-box; + + li { + display: flex; + flex-flow: row; + align-items: center; + box-sizing: border-box; + } + + .tp__breadcrumbs__icon { + display: flex; + flex-flow: row; + align-items: center; + box-sizing: border-box; + fill: var(--modest-text-color); + svg { + width: 1.2rem; + } + } + } + + span { + box-sizing: border-box; + color: var(--modest-text-color); + + } + + &__active { + pointer-events: none; + } + + &__selectable { + cursor: pointer; + &:hover { + color: var(--text-color); + } + } +} \ No newline at end of file diff --git a/webui/scss/_card.scss b/webui/scss/_card.scss index 9679c955a..21d6d4a33 100644 --- a/webui/scss/_card.scss +++ b/webui/scss/_card.scss @@ -1,4 +1,4 @@ -.card { +.tp__card { display: flex; flex-flow: column; background-color: var(--card-background-color); diff --git a/webui/scss/_collapse_panel.scss b/webui/scss/_collapse_panel.scss index b417ade04..cf611cf3c 100644 --- a/webui/scss/_collapse_panel.scss +++ b/webui/scss/_collapse_panel.scss @@ -1,4 +1,4 @@ -.collapse-panel { +.tp__collapse-panel { display: flex; flex-flow: column; gap: 8px; diff --git a/webui/scss/_common.scss b/webui/scss/_common.scss index a2d1a96db..45faa427f 100644 --- a/webui/scss/_common.scss +++ b/webui/scss/_common.scss @@ -1,105 +1,6 @@ $media-size-mobile: 600px; -@mixin container_common($justify-content: flex-start, $padding: 12px) { - display: flex; - justify-content: $justify-content; - gap: 8px; - box-sizing: border-box; - @media (max-width: $media-size-mobile) { - padding: $padding; - } - @media (min-width: $media-size-mobile) { - padding: 2px; - } -} - -@mixin container_vert() { - flex-flow: column; - @include container_common(); -} - -@mixin container_horz() { - flex-flow: row; - @include container_common(); -} - -@mixin container_horz_nowrap($justify-content: flex-start, $padding: 12px) { - flex-flow: row nowrap; - @media (max-width: $media-size-mobile) { - flex-flow: column; - } - @include container_common($justify-content, $padding); -} - -@mixin container_horz_no_padding() { - flex-flow: row; - @include container_common($padding: 0); -} - -@mixin container_vert_no_padding($justify-content: flex-start) { - flex-flow: column; - @include container_common($justify-content, $padding: 0); -} - -@mixin activeOptionButton { - box-sizing: border-box !important; - background-color: var(--option-active-background-color) !important; - color: var(--option-active-color) !important; - font-weight: bold; - svg { - color: var(--option-active-color) !important; - } -} - -@mixin textButton { - border: 1px solid var(--text-button-border-color); - font-size: 1.2rem; - border-radius: var(--text-button-border-radius); - padding: 8px 16px; - background-color: var(--text-button-background-color); - color: var(--text-button-color); - &:focus, - &:hover { - background-color: var(--text-button-hover-background-color); - color: var(--text-button-hover-color); - border: 1px solid transparent; - outline: none; - } -} - -@mixin list { - ul { - list-style-type: none; /* Remove bullets */ - padding: 0; /* Remove padding */ - margin: 0; /* Remove margins */ - li { - padding: 8px; - border-radius: var(--border-radius); - } - } -} - -@mixin list-hover { - ul { - list-style-type: none; /* Remove bullets */ - padding: 0; /* Remove padding */ - margin: 0; /* Remove margins */ - li { - padding: 8px; - border-radius: var(--border-radius); - user-select: none; - cursor: pointer; - - &:hover { - color: var(--popup-hover-color); - background-color: var(--popup-hover-background-color); - } - } - } -} - - -@keyframes pulse-bg { +@keyframes tp__pulse-bg { 0% { background-color: var(--pulse-color-1); } @@ -111,6 +12,6 @@ $media-size-mobile: 600px; } } -.pulse { - animation: pulse-bg 500ms ease-out; +.tp__pulse { + animation: tp__pulse-bg 500ms ease-out; } \ No newline at end of file diff --git a/webui/scss/_component.scss b/webui/scss/_component.scss index 4c6f6715c..3b2213c99 100644 --- a/webui/scss/_component.scss +++ b/webui/scss/_component.scss @@ -2,12 +2,19 @@ @forward "menu_item"; @forward "icon_button"; @forward "text_button"; +@forward "toggle_switch"; +@forward "table"; @forward "app_header"; @forward "app"; @forward "panel"; @forward "card"; +@forward "action_card"; +@forward "status_card"; @forward "collapse_panel"; +@forward "breadcrumbs"; @forward "login"; @forward "sidebar"; @forward "preferences"; @forward "dashboard_view"; +@forward "playlist_view"; +@forward "playlist_assistant"; diff --git a/webui/scss/_dashboard_view.scss b/webui/scss/_dashboard_view.scss index e0f42f223..ccd3713bd 100644 --- a/webui/scss/_dashboard_view.scss +++ b/webui/scss/_dashboard_view.scss @@ -1,6 +1,6 @@ $dashboard-mobile-size: 1280px; -.dashboard { +.tp__dashboard { display: flex; flex-flow: column; box-sizing: border-box; @@ -39,64 +39,11 @@ $dashboard-mobile-size: 1280px; } } - .action-card { - display: flex; - flex-flow: row nowrap; - align-content: flex-start; - flex: 1 1 auto; - gap: var(--gap-default); - fill: var(--card-color); - - &__icon { - fill: inherit; - width: 3rem; - transform: translateY(-0.5rem); - - img { - width: 3rem; - } - } - - &__body { - display: flex; - flex-flow: column; - align-content: flex-start; - flex: 1 1 auto; - gap: var(--gap-small); - } - - &__title { - color: var(--text-color); - font-weight: bold; - font-size: 1.1rem; - } - - &__content { - - } - } - - .status-card { - display: flex; - flex-flow: column; - gap: var(--gap-default); - &__title { - color: var(--modest-text-color); - } - &__body { - color: var(--text-color); - font-size: 1.1rem; - } - &__footer { - color: var(--modest-text-color); - } - } - - .discord { + .tp__discord { fill: var(--discord-color); } - .documentation { + .tp__documentation { fill: var(--documenation-color); svg { diff --git a/webui/scss/_default.scss b/webui/scss/_default.scss index df028a9fe..06f2b10ac 100644 --- a/webui/scss/_default.scss +++ b/webui/scss/_default.scss @@ -21,47 +21,13 @@ button { cursor: pointer; } -.hidden { +.tp__hidden { display: none !important; } -.vert-group { - display: flex; - flex-flow: column; - gap: 20px; - overflow: hidden; - padding: 0 4px 4px 0; -} - select { padding: 2px 4px; } -// -//.tool-button { -// display: flex; -// align-items: center; -// justify-content: center; -// height: 1.1rem; -// width: 1.1rem; -// border-radius: var(--border-radius); -// background-color: var(--tool-button-background-color); -// color: var(--tool-button-color); -// cursor: pointer; -// -// svg { -// width: 1rem; -// height: 1rem; -// } -// -// &:hover { -// color: var(--tool-button-hover-color); -// background-color: var(--tool-button-hover-background-color); -// } -// -// &:active { -// transform: scale(0.9); -// } -//} :not(input[type="text"]) { caret-color: transparent; @@ -69,18 +35,6 @@ select { input[type="text"] { caret-color: auto; } -// -//select { -// height: 2rem; -// border-radius: var(--border-radius); -// font-size: 1.2rem; -// padding: 0 8px; -// cursor: pointer; -// &:hover { -// color: var(--text-button-hover-color); -// background-color: var(--text-button-hover-background-color); -// } -//} option { cursor: pointer; diff --git a/webui/scss/_icon_button.scss b/webui/scss/_icon_button.scss index 32b663a29..1d04aa36e 100644 --- a/webui/scss/_icon_button.scss +++ b/webui/scss/_icon_button.scss @@ -1,4 +1,4 @@ -.icon-button { +.tp__icon-button { display: flex; justify-content: center; align-items: center; diff --git a/webui/scss/_input.scss b/webui/scss/_input.scss index 2c3f5971f..da8ee140d 100644 --- a/webui/scss/_input.scss +++ b/webui/scss/_input.scss @@ -1,4 +1,4 @@ -.input { +.tp__input { display: flex; flex-flow: column; overflow: hidden; diff --git a/webui/scss/_login.scss b/webui/scss/_login.scss index 0f231570d..852f6efea 100644 --- a/webui/scss/_login.scss +++ b/webui/scss/_login.scss @@ -1,6 +1,6 @@ @use 'common'; -.login-view { +.tp__login-view { display: flex; flex-flow: column; gap: 8px; diff --git a/webui/scss/_menu_item.scss b/webui/scss/_menu_item.scss index 06fac6a57..7bf584313 100644 --- a/webui/scss/_menu_item.scss +++ b/webui/scss/_menu_item.scss @@ -1,4 +1,4 @@ -.menu-item { +.tp__menu-item { display: flex; flex-flow: row nowrap; align-items: center; diff --git a/webui/scss/_panel.scss b/webui/scss/_panel.scss index 20858e385..bef36b7bb 100644 --- a/webui/scss/_panel.scss +++ b/webui/scss/_panel.scss @@ -1,10 +1,6 @@ -.panel { +.tp__panel { display: flex; flex-flow: column; - gap: 8px; overflow: hidden; - padding: 60px; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - background-color: var(--card-background-color); + background-color: transparent; } \ No newline at end of file diff --git a/webui/scss/_playlist_assistant.scss b/webui/scss/_playlist_assistant.scss new file mode 100644 index 000000000..a4ac0a094 --- /dev/null +++ b/webui/scss/_playlist_assistant.scss @@ -0,0 +1,3 @@ +.tp__playlist-assistant { + +} \ No newline at end of file diff --git a/webui/scss/_playlist_view.scss b/webui/scss/_playlist_view.scss new file mode 100644 index 000000000..65252a27f --- /dev/null +++ b/webui/scss/_playlist_view.scss @@ -0,0 +1,39 @@ +.tp__playlist-view { + display: flex; + flex-flow: column; + box-sizing: border-box; + flex: 1 1 auto; + width: 100%; + max-width: 1280px; + padding: var(--padding-default) 0; + gap: var(--gap-default); + + &__body { + display: flex; + flex-flow: column; + box-sizing: border-box; + flex: 1 1 auto; + width: 100%; + } +} + +.tp__playlist-create, +.tp__playlist-list { + display: flex; + flex-flow: column; + box-sizing: border-box; + flex: 1 1 auto; + width: 100%; + max-width: 1280px; + gap: var(--gap-default); + + &__header { + display: flex; + flex-flow: row nowrap; + box-sizing: border-box; + flex: 1 1 auto; + width: 100%; + justify-content: space-between; + align-items: center; + } +} \ No newline at end of file diff --git a/webui/scss/_preferences.scss b/webui/scss/_preferences.scss index 06e6c60d8..24ed67121 100644 --- a/webui/scss/_preferences.scss +++ b/webui/scss/_preferences.scss @@ -1,94 +1,24 @@ @use 'common'; -@mixin prefsRoot { - @include common.container_vert_no_padding(flex-start); - align-content: flex-start; - overflow: hidden; -} -@mixin prefsInput { - color: var(--text-color); - background-color: transparent; - padding: 0.2rem 0.5rem; - flex: 1 1 auto; - border: 1px solid var(--border-color); - - &:focus, - &:hover { - outline: none; - } -} - -@mixin prefsToolbar() { - &__toolbar { - display: flex; - flex-flow: row wrap; - flex: 0 0 auto; - justify-content: flex-end; - align-items: center; - gap: var(--gap-default); - background-color: var(--toolbar-bg-color); - padding: 8px 16px; - - label { - flex: 1 1 auto; - font-size: 2rem; - font-weight: bold; - color: var(--title-label-color); - } - - button { - @include common.textButton(); - } - } -} - -@mixin prefsForm() { - &__content { - display: flex; - flex-flow: column; - - &-help { - display: flex; - flex-flow: column; - gap:12px; - border-top: 1px solid var(--border-light-color); - padding-top: 12px; - color: var(--label-color); - - pre { - margin: 0; - } - } - - } - -} - - -.preferences { - @include common.container_horz_no_padding(); +.tp__preferences { overflow: hidden; flex: 1 1 auto; &__content { - @include common.container_horz_nowrap(flex-start, 0); flex: 1 1 auto; overflow: hidden; gap: 0; } &__panels { - @include common.container_vert_no_padding(flex-start); overflow: hidden; flex: 1 1 auto; .panel { - @include common.container_vert(); flex: 1 1 auto; overflow: hidden; .card { - @include common.container_vert(); flex: 1 1 auto; overflow: hidden; } diff --git a/webui/scss/_sidebar.scss b/webui/scss/_sidebar.scss index c7f94b43c..9d60dcd6a 100644 --- a/webui/scss/_sidebar.scss +++ b/webui/scss/_sidebar.scss @@ -2,7 +2,7 @@ $sidebar-width: 270px; -.app-sidebar { +.tp__app-sidebar { display: flex; flex-flow: column; gap: var(--gap-default); diff --git a/webui/scss/_status_card.scss b/webui/scss/_status_card.scss new file mode 100644 index 000000000..a3c686070 --- /dev/null +++ b/webui/scss/_status_card.scss @@ -0,0 +1,15 @@ +.tp__status-card { + display: flex; + flex-flow: column; + gap: var(--gap-default); + &__title { + color: var(--modest-text-color); + } + &__body { + color: var(--text-color); + font-size: 1.1rem; + } + &__footer { + color: var(--modest-text-color); + } +} \ No newline at end of file diff --git a/webui/scss/_table.scss b/webui/scss/_table.scss new file mode 100644 index 000000000..5cfec7ccc --- /dev/null +++ b/webui/scss/_table.scss @@ -0,0 +1,33 @@ +.tp__table { + display: flex; + flex-flow: column; + gap: var(--gap-default); + padding: var(--padding-default) 0; + background-color: var(--card-background-color); + border-radius: var(--border-radius); + border: 1px solid var(--border-color); + overflow: hidden; + + &__container { + display: flex; + flex-flow: column; + overflow-x: auto; + } + + &__table { + width: 100%; + border-collapse: collapse; + + thead { + tr { + th { + font-size: 1.1rem; + font-weight: bold; + text-align: center; + padding: 0.875rem; + background-color: var(--table-header-background-color); + } + } + } + } +} \ No newline at end of file diff --git a/webui/scss/_text_button.scss b/webui/scss/_text_button.scss index 76aa9f541..a33215a30 100644 --- a/webui/scss/_text_button.scss +++ b/webui/scss/_text_button.scss @@ -1,4 +1,4 @@ -.text-button { +.tp__text-button { display: flex; justify-content: center; align-items: center; @@ -32,12 +32,14 @@ } } -.text-button.button-primary { +.tp__text-button.tp__button-primary { background-color: var(--text-button-primary-background-color); color: var(--text-button-primary-color); + fill : var(--text-button-primary-color); &:hover { background-color: var(--text-button-primary-hover-background-color); color: var(--text-button-primary-hover-color); + fill : var(--text-button-primary-hover-color); outline: none; } } \ No newline at end of file diff --git a/webui/scss/_theme.scss b/webui/scss/_theme.scss index e19b504ff..8588da675 100644 --- a/webui/scss/_theme.scss +++ b/webui/scss/_theme.scss @@ -8,6 +8,8 @@ --padding-default: 1.5rem; --padding-large: 2rem; + --primary-color: #124a9b; + --action-item-background-color: #242427; --text-color: #ffffff; @@ -17,12 +19,12 @@ --input-border-color: #454548; --input-focus-border-color: #12499a; --input-autofill-background-color: #323a48; - --button-background-color: #124a9b; + --button-background-color: var(--primary-color); --border-color: #222224; --menu-text-color: #e2e2e5; --menu-icon-color: #707079; --menu-item-hover-background-color: var(--action-item-background-color); - --menu-text-active-color: #124a9b; + --menu-text-active-color: var(--primary-color); --menu-item-active-background-color: var(--menu-item-hover-background-color); --icon-button-hover-color: #8f8f97; --card-background-color: #18181b; @@ -32,11 +34,17 @@ --text-button-color: #ffffff; --text-button-border-color: #2F2F31; --text-button-hover-color: #303032; - --text-button-primary-background-color: #124a9b; + --text-button-primary-background-color: var(--primary-color); --text-button-primary-color: #ffffff; --text-button-primary-hover-background-color: #2f68be; --text-button-primary-hover-color: #ffffff; + --toggle-switch-on-background-color: var(--primary-color); + --toggle-switch-off-background-color: #888888; + --toggle-switch-toggle-color: #ffffff; + + --table-header-background-color: #242427; + --discord-color: #5764f0; --documenation-color: #2b944b; diff --git a/webui/scss/_toggle_switch.scss b/webui/scss/_toggle_switch.scss new file mode 100644 index 000000000..386717c35 --- /dev/null +++ b/webui/scss/_toggle_switch.scss @@ -0,0 +1,44 @@ +$toggle-switch-width: 3.2rem; +$toggle-switch-height: 1.6rem; +$toggle-switch-toggle-padding: 3px; +$toggle-switch-toggle-width: calc($toggle-switch-height - $toggle-switch-toggle-padding * 2); +$toggle-switch-toggle-end-pos: calc($toggle-switch-width - ($toggle-switch-toggle-width + $toggle-switch-toggle-padding * 2)); + +.tp__toggle-switch { + position: relative; + display: inline-block; + width: $toggle-switch-width; + height: $toggle-switch-height; + input { + opacity: 0; width: 0; height: 0 + } + input:checked + span { + background-color: var(--toggle-switch-on-background-color); + } + input:checked + span > span { + transform: translateX($toggle-switch-toggle-end-pos); + } + &__track { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: var(--toggle-switch-off-background-color); + border-radius: $toggle-switch-toggle-width; + transition: 0.3s; + + } + &__toggle { + position: absolute; + height: $toggle-switch-toggle-width; + width: $toggle-switch-toggle-width; + left: $toggle-switch-toggle-padding; + bottom: $toggle-switch-toggle-padding; + background-color: var(--toggle-switch-toggle-color); + border-radius: 50%; + transition: 0.3s; + transform: translateX(0); + } + &__active { + + } +} \ No newline at end of file diff --git a/webui/src/app/components/breadcrumbs.rs b/webui/src/app/components/breadcrumbs.rs new file mode 100644 index 000000000..3665a573f --- /dev/null +++ b/webui/src/app/components/breadcrumbs.rs @@ -0,0 +1,65 @@ +use std::rc::Rc; +use std::str::FromStr; +use wasm_bindgen::JsCast; +use yew::prelude::*; +use crate::app::components::AppIcon; + +#[derive(Properties, PartialEq, Clone)] +pub struct BreadcrumbsProps { + pub items: Rc>, + #[prop_or_default] + pub onclick: Callback<(String, usize)>, +} +#[function_component] +pub fn Breadcrumbs(props: &BreadcrumbsProps) -> Html { + let len = props.items.len(); + + let handle_click = { + let click = props.onclick.clone(); + Callback::from(move |e: MouseEvent| { + e.prevent_default(); + if let Some(target) = e.target() { + if let Ok(element) = target.dyn_into::() { + if let Some(data_name) = element.get_attribute("data-name") { + if let Some(data_index) = element.get_attribute("data-index") { + if let Ok(index) = usize::from_str(&data_index) { + click.emit((data_name, index)); + } + } + } + } + } + }) + }; + + html! { + + } +} diff --git a/webui/src/app/components/card.rs b/webui/src/app/components/card.rs index da49d7e41..4ae55ea74 100644 --- a/webui/src/app/components/card.rs +++ b/webui/src/app/components/card.rs @@ -14,7 +14,7 @@ pub fn Card(props: &CardProps) -> Html { }; html! { context={context}> -
+
{ for props.children.iter() }
> diff --git a/webui/src/app/components/collapse_panel.rs b/webui/src/app/components/collapse_panel.rs index 78db2e3a2..e42b9d596 100644 --- a/webui/src/app/components/collapse_panel.rs +++ b/webui/src/app/components/collapse_panel.rs @@ -19,13 +19,13 @@ pub fn CollapsePanel(props: &CollapsePanelProps) -> Html { }; html! { -
-
+
+
{ props.title.clone() }
if *expanded { -
+
{ for props.children.iter() }
} diff --git a/webui/src/app/components/dashboard/action_card.rs b/webui/src/app/components/dashboard/action_card.rs index ff466704b..f562b52cb 100644 --- a/webui/src/app/components/dashboard/action_card.rs +++ b/webui/src/app/components/dashboard/action_card.rs @@ -22,15 +22,15 @@ pub struct ActionProps { pub fn ActionCard(props: &ActionProps) -> Html { html! { -
-
+
+
-
- +
+ {props.title.clone()} - + { props.subtitle.clone() } { if !props.subtitle_html.is_empty() { diff --git a/webui/src/app/components/dashboard/discord_action_card.rs b/webui/src/app/components/dashboard/discord_action_card.rs index 9231015fd..2e13e8063 100644 --- a/webui/src/app/components/dashboard/discord_action_card.rs +++ b/webui/src/app/components/dashboard/discord_action_card.rs @@ -22,7 +22,7 @@ pub fn DiscordActionCard() -> Html { }; html! { - diff --git a/webui/src/app/components/dashboard/documentation_action_card.rs b/webui/src/app/components/dashboard/documentation_action_card.rs index e63389151..10bce92db 100644 --- a/webui/src/app/components/dashboard/documentation_action_card.rs +++ b/webui/src/app/components/dashboard/documentation_action_card.rs @@ -21,7 +21,7 @@ pub fn DocumentationActionCard() -> Html { }; html! { - diff --git a/webui/src/app/components/dashboard/ipinfo_action_card.rs b/webui/src/app/components/dashboard/ipinfo_action_card.rs index 22c695ab6..85089f917 100644 --- a/webui/src/app/components/dashboard/ipinfo_action_card.rs +++ b/webui/src/app/components/dashboard/ipinfo_action_card.rs @@ -24,7 +24,7 @@ pub fn IpinfoActionCard() -> Html { if exists { let card_ctx_2 = card_ctx.clone(); if let Some(card_context) = card_ctx_1.as_ref() { - card_context.custom_class.set("pulse".to_string()); + card_context.custom_class.set("tp__pulse".to_string()); } let services_ctx = services_ctx.clone(); let ip_address_state = ip_address_state.clone(); @@ -87,7 +87,7 @@ pub fn IpinfoActionCard() -> Html { } html! { - Html { - html! { -
- +
+ {props.title.clone()} -
+
{props.data.clone()}
- diff --git a/webui/src/app/components/dashboard_view.rs b/webui/src/app/components/dashboard_view.rs index 0a3c64418..6854d3d54 100644 --- a/webui/src/app/components/dashboard_view.rs +++ b/webui/src/app/components/dashboard_view.rs @@ -40,12 +40,12 @@ pub fn DashboardView() -> Html { } html! { -
-
+
+

{ translate.t("LABEL.DASHBOARD")}

-
-
+
+
@@ -55,7 +55,7 @@ pub fn DashboardView() -> Html {
-
+
diff --git a/webui/src/app/components/home.rs b/webui/src/app/components/home.rs index 4cbae3b43..1ba60daf8 100644 --- a/webui/src/app/components/home.rs +++ b/webui/src/app/components/home.rs @@ -1,5 +1,5 @@ use yew::prelude::*; -use crate::app::components::{IconButton, Sidebar, DashboardView}; +use crate::app::components::{IconButton, Sidebar, DashboardView, PlaylistView}; use crate::model::ViewType; use crate::hooks::use_service_context; @@ -8,7 +8,7 @@ pub fn Home() -> Html { let services = use_service_context(); let app_title = services.config.ui_config.app_title.as_ref().map_or("tuliprox", |v| v.as_str()); - let view_visible = use_state(|| ViewType::Dashboard); + let view_visible = use_state(|| ViewType::Playlists); let handle_logout = { let services_ctx = services.clone(); @@ -24,13 +24,13 @@ pub fn Home() -> Html { //
html! { -
+
-
-
+
+
{app_title} -
+
// @@ -47,10 +47,11 @@ pub fn Home() -> Html {
-
+
{ match *view_visible { ViewType::Dashboard => html! { }, + ViewType::Playlists => html! { }, _ => html! {
{"Unknown view"}
}, } } diff --git a/webui/src/app/components/icon_button.rs b/webui/src/app/components/icon_button.rs index ac25750eb..4b23b32b8 100644 --- a/webui/src/app/components/icon_button.rs +++ b/webui/src/app/components/icon_button.rs @@ -22,7 +22,7 @@ pub fn IconButton(props: &IconButtonProps) -> Html { }; html! { - } diff --git a/webui/src/app/components/input.rs b/webui/src/app/components/input.rs index 94aba7ce0..5b5dc87e9 100644 --- a/webui/src/app/components/input.rs +++ b/webui/src/app/components/input.rs @@ -22,14 +22,14 @@ pub struct InputProps { #[function_component] pub fn Input(props: &InputProps) -> Html { html! { -
+
{ if props.label.is_some() { html! { } } else { html!{} } } -
+
Html { }; html! { -