From 0a9fc09f4790c502b665196de42b9dc160f91e26 Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 14 Aug 2025 13:42:47 +0200 Subject: [PATCH] Epg Config view --- frontend/public/assets/i18n/en.json | 15 +- .../scss/app/components/_reveal_content.scss | 4 + frontend/src/app/components/config/macros.rs | 2 +- .../config/messaging_config_view.rs | 4 +- .../config/schedules_config_view.rs | 2 +- .../components/config/video_config_view.rs | 4 +- .../components/playlist/epg_config_view.rs | 135 ++++++++++++++++++ .../app/components/playlist/input_table.rs | 11 +- frontend/src/app/components/playlist/mod.rs | 2 + shared/src/model/config/epg_smart_match.rs | 17 +++ shared/src/model/config/input.rs | 18 ++- 11 files changed, 195 insertions(+), 19 deletions(-) create mode 100644 frontend/src/app/components/playlist/epg_config_view.rs diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index f6da81c0f..050be30c5 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -228,7 +228,20 @@ "FIRMWARE_VERSION": "Firmware Version", "DEVICE_TYPE": "Device Type", "DEVICE_UDN": "Device UDN", - "TUNER_COUNT": "Tuner Count" + "TUNER_COUNT": "Tuner Count", + "SOURCE": "Source", + "PRIORITY": "Priority", + "LOGO_OVERRIDE": "logo Override", + "SMART_EPG": "Smart Epg", + "DEFAULT_EPG": "Normal Epg", + "EPG_SMART_MATCH": "EPG Smart Match", + "FUZZY_MATCHING": "Fuzzy Matching", + "MATCH_THRESHOLD": "Match Threshold", + "BEST_MATCH_THRESHOLD": "Best Match Threshold", + "NORMALIZE_REGEX": "Normalize Regex", + "NAME_PREFIX": "Name Prefix", + "NAME_PREFIX_SEPARATOR":"Name Prefix Separator", + "STRIP": "Strip" }, "TABLE": { "EMPTY": "", diff --git a/frontend/scss/app/components/_reveal_content.scss b/frontend/scss/app/components/_reveal_content.scss index 224bf9ae0..bf78ff627 100644 --- a/frontend/scss/app/components/_reveal_content.scss +++ b/frontend/scss/app/components/_reveal_content.scss @@ -8,7 +8,10 @@ &:hover { fill: var(--text-color); + cursor: pointer; + filter: brightness(1.2); } + .svg-icon { width: 1.2rem; height: 1.2rem; @@ -22,6 +25,7 @@ text-overflow: ellipsis; max-height: 2rem; max-width: var(--max-table-cell-width); + pointer-events: none; } } \ No newline at end of file diff --git a/frontend/src/app/components/config/macros.rs b/frontend/src/app/components/config/macros.rs index 977a7e3ea..37ea08bea 100644 --- a/frontend/src/app/components/config/macros.rs +++ b/frontend/src/app/components/config/macros.rs @@ -83,7 +83,7 @@ macro_rules! config_field { #[macro_export] macro_rules! config_field_child { - ($config:expr, $label:expr, $body:block) => { + ($label:expr, $body:block) => { html! {
diff --git a/frontend/src/app/components/config/messaging_config_view.rs b/frontend/src/app/components/config/messaging_config_view.rs index e8939e749..b6d9cea76 100644 --- a/frontend/src/app/components/config/messaging_config_view.rs +++ b/frontend/src/app/components/config/messaging_config_view.rs @@ -16,7 +16,7 @@ pub fn MessagingConfigView() -> Html {

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

{ config_field!(entry, translate.t("LABEL.BOT_TOKEN"), bot_token) } - { config_field_child!(entry, translate.t("LABEL.CHAT_ID"), { + { config_field_child!(translate.t("LABEL.CHAT_ID"), { html! {
{ @@ -102,7 +102,7 @@ pub fn MessagingConfigView() -> Html { html! { <>
- { config_field_child!(entry, translate.t("LABEL.NOTIFY_ON"), { + { config_field_child!(translate.t("LABEL.NOTIFY_ON"), { html! {
{ for messaging.notify_on.iter().map(|t| html! { }) }
} diff --git a/frontend/src/app/components/config/schedules_config_view.rs b/frontend/src/app/components/config/schedules_config_view.rs index e98acdbff..4f5a676a5 100644 --- a/frontend/src/app/components/config/schedules_config_view.rs +++ b/frontend/src/app/components/config/schedules_config_view.rs @@ -20,7 +20,7 @@ pub fn SchedulesConfigView() -> Html { { for schedules.iter().map(|entry| html! {
{ config_field!(entry, translate.t("LABEL.SCHEDULE"), schedule) } - { config_field_child!(entry, translate.t("LABEL.TARGETS"), { + { config_field_child!(translate.t("LABEL.TARGETS"), { html! {
{ match entry.targets.as_ref() { None => html! { }, diff --git a/frontend/src/app/components/config/video_config_view.rs b/frontend/src/app/components/config/video_config_view.rs index c9677b520..838f6b456 100644 --- a/frontend/src/app/components/config/video_config_view.rs +++ b/frontend/src/app/components/config/video_config_view.rs @@ -12,7 +12,7 @@ pub fn VideoConfigView() -> Html { let render_extensions = |extensions: &Vec| html! { - { config_field_child!(entry, translate.t("LABEL.EXTENSIONS"), { + { config_field_child!(translate.t("LABEL.EXTENSIONS"), { html! {
{ @@ -31,7 +31,7 @@ pub fn VideoConfigView() -> Html { { config_field_bool!(entry, translate.t("LABEL.ORGANIZE_INTO_DIRECTORIES"), organize_into_directories) } { config_field_optional!(entry, translate.t("LABEL.DIRECTORY"), directory) } { config_field_optional!(entry, translate.t("LABEL.EPISODE_PATTERN"), episode_pattern) } - { config_field_child!(entry.headers, translate.t("LABEL.HEADERS"), { + { config_field_child!(translate.t("LABEL.HEADERS"), { html! {
    diff --git a/frontend/src/app/components/playlist/epg_config_view.rs b/frontend/src/app/components/playlist/epg_config_view.rs new file mode 100644 index 000000000..02405733d --- /dev/null +++ b/frontend/src/app/components/playlist/epg_config_view.rs @@ -0,0 +1,135 @@ +use crate::app::components::{Card, Chip, NoContent}; +use crate::{config_field, config_field_bool, config_field_child, config_field_optional}; +use shared::model::{EpgConfigDto, EpgSmartMatchConfigDto, EpgSourceDto}; +use yew::prelude::*; +use yew_i18n::use_translation; + +#[derive(Properties, PartialEq, Clone)] +pub struct EpgConfigViewProps { + #[prop_or_default] + pub epg: Option +} + +#[function_component] +pub fn EpgConfigView(props: &EpgConfigViewProps) -> Html { + + let translate = use_translation(); + + let render_sources = |sources: Option<&Vec>| { + let render_empty_sources = || html! { + +

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

    + +
    + }; + + if let Some(sources) = sources { + if sources.is_empty() { + render_empty_sources() + } else { + html!{ + for sources.iter().map(|entry| html! { + +

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

    + { config_field!(entry, translate.t("LABEL.URL"), url) } + { config_field!(entry, translate.t("LABEL.PRIORITY"), priority) } + { config_field_bool!(entry, translate.t("LABEL.LOGO_OVERRIDE"), logo_override) } +
    + })} + } + } else { + render_empty_sources() + } + }; + + let render_smart_match = |epg_smart_match: Option<&EpgSmartMatchConfigDto>| { + let render_empty_smart_match = || html! { + +

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

    + +
    + }; + + if let Some(entry) = epg_smart_match { + html!{ + +

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

    + { config_field_bool!(entry, translate.t("LABEL.ENABLED"), enabled) } + { config_field_bool!(entry, translate.t("LABEL.FUZZY_MATCHING"), fuzzy_matching) } + { config_field!(entry, translate.t("LABEL.MATCH_THRESHOLD"), match_threshold) } + { config_field!(entry, translate.t("LABEL.BEST_MATCH_THRESHOLD"), best_match_threshold) } + { config_field_optional!(entry, translate.t("LABEL.NORMALIZE_REGEX"), normalize_regex) } + { config_field!(entry, translate.t("LABEL.NAME_PREFIX"), name_prefix) } + { config_field_child!(translate.t("LABEL.NAME_PREFIX_SEPARATOR"), { + html! { +
    + { + if let Some(name_prefixe_seperators) = entry.name_prefix_separator.as_ref() { + if name_prefixe_seperators.is_empty() { + html! {} + } else { + html! { + for name_prefixe_seperators.iter().map(|value| html! { + + }) + } + } + } else { + html! {} + } + } +
    + } + })} + { config_field_child!(translate.t("LABEL.STRIP"), { + html! { +
    + { + if let Some(strip) = entry.strip.as_ref() { + if strip.is_empty() { + html! {} + } else { + html! { + for strip.iter().map(|value| html! { + + }) + } + } + } else { + html! {} + } + } +
    + } + })} +
    + } + } else { + render_empty_smart_match() + } + }; + + let render_empty = || html! { +
    + {render_smart_match(None)} + {render_sources(None)} +
    + }; + + html! { +
    + { + if let Some(epg) = &props.epg { + html! { +
    + {render_smart_match(epg.smart_match.as_ref())} + {render_sources(epg.sources.as_ref())} +
    + } + } else { + { render_empty() } + } + } +
    + } +} diff --git a/frontend/src/app/components/playlist/input_table.rs b/frontend/src/app/components/playlist/input_table.rs index 32f1f3c5b..04291737f 100644 --- a/frontend/src/app/components/playlist/input_table.rs +++ b/frontend/src/app/components/playlist/input_table.rs @@ -1,6 +1,6 @@ use std::fmt::Display; use crate::app::components::popup_menu::PopupMenu; -use crate::app::components::{convert_bool_to_chip_style, AppIcon, BatchInputContentView, Chip, HideContent, InputHeaders, InputOptions, InputTypeView, RevealContent, StagedInputView, Table, TableDefinition}; +use crate::app::components::{convert_bool_to_chip_style, AppIcon, BatchInputContentView, Chip, EpgConfigView, HideContent, InputHeaders, InputOptions, InputTypeView, RevealContent, StagedInputView, Table, TableDefinition}; use std::rc::Rc; use std::str::FromStr; use yew::platform::spawn_local; @@ -117,7 +117,14 @@ pub fn InputTable(props: &InputTableProps) -> Html { 9 => html! { dto.priority.to_string() }, 10 => html! { dto.max_connections.to_string() }, 11 => html! { dto.method.to_string() }, - 12 => html! { /* TODO */ }, + 12 => html_if!(dto.epg.is_some(), + { + })}}> + + }), 13 => html! { }, 14 => html_if!(dto.staged.is_some(), { diff --git a/frontend/src/app/components/playlist/mod.rs b/frontend/src/app/components/playlist/mod.rs index 2d997e04b..666a4bfd3 100644 --- a/frontend/src/app/components/playlist/mod.rs +++ b/frontend/src/app/components/playlist/mod.rs @@ -17,6 +17,7 @@ mod playlist_explorer; mod playlist_update_view; mod mapper_script_view; mod mapper_counter_view; +mod epg_config_view; use std::rc::Rc; use yew_i18n::YewI18n; @@ -40,6 +41,7 @@ pub use self::playlist_explorer_view::*; pub use self::playlist_update_view::*; pub use self::mapper_script_view::*; pub use self::mapper_counter_view::*; +pub use self::epg_config_view::*; pub fn make_tags(data: &[(bool, &str)], translate: &YewI18n) -> Vec> { data.iter() diff --git a/shared/src/model/config/epg_smart_match.rs b/shared/src/model/config/epg_smart_match.rs index d2c3dd535..824ef75ef 100644 --- a/shared/src/model/config/epg_smart_match.rs +++ b/shared/src/model/config/epg_smart_match.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use log::warn; use crate::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKind}; @@ -10,6 +11,22 @@ pub enum EpgNamePrefix { Prefix(String), } +impl EpgNamePrefix { + const IGNORE: &'static str = "Ignore"; + const SUFFIX: &'static str = "Suffix"; + const PREFIX: &'static str = "Prefix"; +} + +impl Display for EpgNamePrefix { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Ignore => write!(f, "{}", Self::IGNORE), + Self::Suffix(s) => write!(f, "{}({s})", Self::SUFFIX), + Self::Prefix(s) => write!(f, "{}({s})", Self::PREFIX), + } + } +} + #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct EpgSmartMatchConfigDto { diff --git a/shared/src/model/config/input.rs b/shared/src/model/config/input.rs index 64b605f75..416daae02 100644 --- a/shared/src/model/config/input.rs +++ b/shared/src/model/config/input.rs @@ -1,6 +1,6 @@ use crate::error::{TuliproxError, TuliproxErrorKind}; use crate::model::EpgConfigDto; -use crate::utils::{default_as_true, get_base_url_from_str, get_credentials_from_url_str, get_trimmed_string, sanitize_sensitive_info, trim_last_slash}; +use crate::utils::{default_as_true, get_credentials_from_url_str, get_trimmed_string, sanitize_sensitive_info, trim_last_slash}; use crate::{check_input_credentials, check_input_connections, create_tuliprox_error_result, handle_tuliprox_error_result_list, info_err}; use enum_iterator::Sequence; use std::collections::{HashMap, HashSet}; @@ -323,16 +323,14 @@ impl ConfigInputDto { if username.is_none() || password.is_none() || base_url.is_none(){ Err(format!("auto_epg is enabled for input {}, but no credentials could be extracted", self.name)) + } else if base_url.is_some() { + let provider_epg_url = format!("{}/xmltv.php?username={}&password={}", + trim_last_slash(&base_url.unwrap_or_default()), + username.unwrap_or_default(), + password.unwrap_or_default()); + Ok(provider_epg_url) } else { - if base_url.is_some() { - let provider_epg_url = format!("{}/xmltv.php?username={}&password={}", - trim_last_slash(&base_url.unwrap_or_default()), - username.unwrap_or_default(), - password.unwrap_or_default()); - Ok(provider_epg_url) - } else { - Err(format!("auto_epg is enabled for input {}, but url could not be parsed {}", self.name, sanitize_sensitive_info(&self.url))) - } + Err(format!("auto_epg is enabled for input {}, but url could not be parsed {}", self.name, sanitize_sensitive_info(&self.url))) } };