From b65eb87417f65237ea81abbbf591c2cd6d07f39c Mon Sep 17 00:00:00 2001 From: euzu Date: Tue, 8 Jul 2025 18:23:03 +0200 Subject: [PATCH] webui --- backend/src/api/api_utils.rs | 4 +- backend/src/api/endpoints/xtream_api.rs | 20 +++-- backend/src/processing/parser/xtream.rs | 5 +- backend/src/utils/file/csv_input_reader.rs | 4 +- backend/src/utils/trakt/client.rs | 3 +- shared/src/model/config/input.rs | 7 +- shared/src/model/config/sort.rs | 11 +++ shared/src/utils/string_utils.rs | 19 ++++ webui/public/assets/i18n/en.json | 7 +- webui/scss/_default.scss | 5 ++ webui/scss/app/_component.scss | 3 + webui/scss/app/components/_popup_menu.scss | 5 ++ .../playlist/target/_target_common.scss | 41 +++++++++ .../playlist/target/_target_output.scss | 42 --------- .../playlist/target/_target_rename.scss | 41 +++++++++ .../playlist/target/_target_sort.scss | 49 ++++++++++ .../playlist/target/hdhomerun_output.rs | 14 +-- .../components/playlist/target/m3u_output.rs | 8 +- .../src/app/components/playlist/target/mod.rs | 3 + .../components/playlist/target/strm_output.rs | 16 ++-- .../playlist/target/target_rename.rs | 22 +++++ .../components/playlist/target/target_sort.rs | 89 +++++++++++++++++-- .../playlist/target/target_watch.rs | 2 - .../playlist/target/xtream_output.rs | 21 ++--- .../app/components/playlist/target_table.rs | 32 +++++-- webui/src/app/components/reveal_content.rs | 2 +- webui/src/hooks/use_service_context.rs | 7 +- webui/src/services/mod.rs | 2 + webui/src/services/playlist_service.rs | 22 +++++ webui/src/services/requests.rs | 14 +-- 30 files changed, 399 insertions(+), 121 deletions(-) create mode 100644 webui/scss/app/components/playlist/target/_target_common.scss create mode 100644 webui/scss/app/components/playlist/target/_target_rename.scss create mode 100644 webui/scss/app/components/playlist/target/_target_sort.scss create mode 100644 webui/src/app/components/playlist/target/target_rename.rs create mode 100644 webui/src/services/playlist_service.rs diff --git a/backend/src/api/api_utils.rs b/backend/src/api/api_utils.rs index 2f00fc8ce..0fa7defd7 100644 --- a/backend/src/api/api_utils.rs +++ b/backend/src/api/api_utils.rs @@ -30,7 +30,7 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use log::{debug, error, log_enabled, trace}; use reqwest::StatusCode; use shared::model::{InputFetchMethod, PlaylistEntry, PlaylistItemType, TargetType, UserConnectionPermission, XtreamCluster}; -use shared::utils::{default_grace_period_millis, human_readable_byte_size}; +use shared::utils::{default_grace_period_millis, human_readable_byte_size, trim_slash}; use shared::utils::{extract_extension_from_url, replace_url_extension, sanitize_sensitive_info, DASH_EXT, HLS_EXT}; use std::borrow::Cow; use std::collections::HashMap; @@ -439,7 +439,7 @@ where if self.action_path.is_empty() { format!("{provider_id}{extension}") } else { - format!("{}/{provider_id}{extension}", self.action_path) + format!("{}/{provider_id}{extension}", trim_slash(self.action_path)) } } } diff --git a/backend/src/api/endpoints/xtream_api.rs b/backend/src/api/endpoints/xtream_api.rs index 08ec0726f..0dc440796 100644 --- a/backend/src/api/endpoints/xtream_api.rs +++ b/backend/src/api/endpoints/xtream_api.rs @@ -20,7 +20,7 @@ use shared::error::info_err; use shared::error::{str_to_io_error, TuliproxError, TuliproxErrorKind}; use crate::utils::trace_if_enabled; use crate::utils::xtream::create_vod_info_from_item; -use shared::utils::{extract_extension_from_url, generate_playlist_uuid, get_u32_from_serde_value, hex_encode, sanitize_sensitive_info, HLS_EXT}; +use shared::utils::{extract_extension_from_url, generate_playlist_uuid, get_u32_from_serde_value, hex_encode, sanitize_sensitive_info, trim_slash, HLS_EXT}; use crate::utils::{request, xtream}; use crate::auth::Fingerprint; use axum::http::{HeaderMap, StatusCode}; @@ -150,14 +150,16 @@ pub(in crate::api) fn get_xtream_player_api_stream_url( | ApiStreamContext::Series | ApiStreamContext::Timeshift => context.to_string() }; - let ctx_path = if ctx.is_empty() { String::new() } else { format!("{ctx}/") }; - Some(format!("{}/{}{}/{}/{}", - &input_user_info.base_url, - ctx_path, - &input_user_info.username, - &input_user_info.password, - action_path - )) + let mut parts = vec![ + trim_slash(&input_user_info.base_url), + trim_slash(&ctx), + trim_slash(&input_user_info.username), + trim_slash(&input_user_info.password), + trim_slash(action_path), + ]; + parts.retain(|s| !s.is_empty()); + Some(parts.join("/")) + } else if !fallback_url.is_empty() { Some(String::from(fallback_url)) } else { diff --git a/backend/src/processing/parser/xtream.rs b/backend/src/processing/parser/xtream.rs index 1da8ba607..bbd7d64c9 100644 --- a/backend/src/processing/parser/xtream.rs +++ b/backend/src/processing/parser/xtream.rs @@ -2,7 +2,7 @@ use shared::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKi use crate::model::ConfigInput; use shared::model::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; use crate::model::{XtreamCategory, XtreamSeriesInfo, XtreamSeriesInfoEpisode, XtreamStream}; -use shared::utils::generate_playlist_uuid; +use shared::utils::{generate_playlist_uuid, trim_last_slash}; use crate::utils::xtream::{get_xtream_stream_url_base}; use serde_json::Value; use std::collections::HashMap; @@ -80,6 +80,7 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &str, password: &str, stream_id: u32, container_extension: Option<&String>, live_stream_use_prefix: bool, live_stream_without_extension: bool) -> String { + let url = trim_last_slash(url); let stream_base_url = match xtream_cluster { XtreamCluster::Live => { let ctx_path = if live_stream_use_prefix { "live/" } else { "" }; @@ -91,7 +92,7 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str, format!("{url}/movie/{username}/{password}/{stream_id}.{ext}") } XtreamCluster::Series => - format!("{}&action={}&series_id={stream_id}", get_xtream_stream_url_base(url, username, password), crate::model::XC_ACTION_GET_SERIES_INFO) + format!("{}&action={}&series_id={stream_id}", get_xtream_stream_url_base(url.as_ref(), username, password), crate::model::XC_ACTION_GET_SERIES_INFO) }; stream_base_url } diff --git a/backend/src/utils/file/csv_input_reader.rs b/backend/src/utils/file/csv_input_reader.rs index 68f8d8d8a..d802b2f13 100644 --- a/backend/src/utils/file/csv_input_reader.rs +++ b/backend/src/utils/file/csv_input_reader.rs @@ -9,7 +9,7 @@ use std::io::{BufRead, Cursor, Error}; use std::path::PathBuf; use url::Url; use shared::model::InputType; -use shared::utils::get_credentials_from_url; +use shared::utils::{get_credentials_from_url, trim_last_slash}; const CSV_SEPARATOR: char = ';'; const HEADER_PREFIX: char = '#'; @@ -33,7 +33,7 @@ fn csv_assign_mandatory_fields(alias: &mut ConfigInputAlias, input_type: InputTy alias.url = url.origin().ascii_serialization().to_string(); } else if input_type == InputType::M3uBatch && alias.username.is_some() && alias.password.is_some() { alias.url = format!("{}/get.php?username={}&password={}&type=m3u_plus", - url.origin().ascii_serialization(), + trim_last_slash(&url.origin().ascii_serialization()), alias.username.as_deref().unwrap_or(""), alias.password.as_deref().unwrap_or("") ); diff --git a/backend/src/utils/trakt/client.rs b/backend/src/utils/trakt/client.rs index 234cb6df0..73c5f616c 100644 --- a/backend/src/utils/trakt/client.rs +++ b/backend/src/utils/trakt/client.rs @@ -4,6 +4,7 @@ use reqwest::header::{HeaderMap, HeaderValue}; use std::sync::Arc; use log::{debug, info}; use shared::error::{info_err, TuliproxErrorKind}; +use shared::utils::trim_last_slash; use super::errors::{handle_trakt_api_error}; pub struct TraktClient { @@ -34,7 +35,7 @@ impl TraktClient { } fn build_list_url(&self, user: &str, list_slug: &str) -> String { - format!("{}/users/{user}/lists/{list_slug}/items", self.api_config.url) + format!("{}/users/{user}/lists/{list_slug}/items", trim_last_slash(&self.api_config.url)) } pub async fn get_list_items(&self, list_config: &TraktListConfig) -> Result, TuliproxError> { diff --git a/shared/src/model/config/input.rs b/shared/src/model/config/input.rs index 14e6e91a7..109e7416f 100644 --- a/shared/src/model/config/input.rs +++ b/shared/src/model/config/input.rs @@ -5,7 +5,7 @@ use enum_iterator::Sequence; use crate::{check_input_credentials, create_tuliprox_error_result, handle_tuliprox_error_result_list, info_err}; 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}; +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 log::debug; @@ -209,7 +209,10 @@ impl ConfigInputDto { } else { let base_url = get_base_url_from_str(&self.url); if base_url.is_some() { - let provider_epg_url = format!("{}/xmltv.php?username={}&password={}", base_url.unwrap_or_default(), username.unwrap_or_default(), password.unwrap_or_default()); + 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))) diff --git a/shared/src/model/config/sort.rs b/shared/src/model/config/sort.rs index a41dedf37..5713b23fd 100644 --- a/shared/src/model/config/sort.rs +++ b/shared/src/model/config/sort.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use crate::error::{TuliproxError, TuliproxErrorKind}; use crate::foundation::filter::{apply_templates_to_pattern, apply_templates_to_pattern_single}; use crate::model::{ItemField, PatternTemplate, TemplateValue}; @@ -24,6 +25,16 @@ pub enum SortOrder { Desc, } +impl Display for SortOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + SortOrder::Asc => "asc".to_string(), + SortOrder::Desc => "desc".to_string(), + }; + write!(f, "{}", str) + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct ConfigSortGroupDto { diff --git a/shared/src/utils/string_utils.rs b/shared/src/utils/string_utils.rs index 572d290db..051501913 100644 --- a/shared/src/utils/string_utils.rs +++ b/shared/src/utils/string_utils.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + pub trait Capitalize { fn capitalize(&self) -> String; } @@ -63,6 +65,23 @@ pub fn get_non_empty_str<'a>(first: &'a str, second: &'a str, third: &'a str) -> } } +pub fn trim_slash(s: &str) -> Cow { + let trimmed = s.trim_matches('/'); + if trimmed.len() == s.len() { + Cow::Borrowed(s) // Keine Änderung → kein Clone + } else { + Cow::Owned(trimmed.to_string()) // Änderung → neue String + } +} + +pub fn trim_last_slash(s: &str) -> Cow { + if s.ends_with('/') { + Cow::Owned(s[..s.len() - 1].to_string()) + } else { + Cow::Borrowed(s) + } +} + #[cfg(test)] mod test { use std::collections::HashSet; diff --git a/webui/public/assets/i18n/en.json b/webui/public/assets/i18n/en.json index 5d367a858..a4fc19639 100644 --- a/webui/public/assets/i18n/en.json +++ b/webui/public/assets/i18n/en.json @@ -158,7 +158,12 @@ "CLEANUP": "Cleanup", "PROPERTIES": "Properties", "DEVICE": "Device", - "USE_OUTPUT": "Use output" + "USE_OUTPUT": "Use output", + "MATCH_AS_ASCII": "Match as ascii", + "SORT_SETTINGS": "Sort Settings", + "ORDER": "Order", + "SEQUENCE": "Sequence", + "GROUPS": "Groups" }, "TABLE": { "EMPTY": "", diff --git a/webui/scss/_default.scss b/webui/scss/_default.scss index 414980fc7..32264fe5e 100644 --- a/webui/scss/_default.scss +++ b/webui/scss/_default.scss @@ -65,6 +65,11 @@ option { fill: var(--attention-color) !important; } +.tp__update_action { + color: var(--active-color) !important; + fill: var(--active-color) !important; +} + .svg-icon { pointer-events: none; width: 1rem; diff --git a/webui/scss/app/_component.scss b/webui/scss/app/_component.scss index 03b864d85..62d3c8b45 100644 --- a/webui/scss/app/_component.scss +++ b/webui/scss/app/_component.scss @@ -19,6 +19,7 @@ @forward "components/playlist/playlist_view"; @forward "components/playlist/assistant/playlist_assistant"; @forward "components/reveal_content"; +@forward "components/playlist/target/target_common"; @forward "components/playlist/target/target_watch"; @forward "components/playlist/target/target_options"; @forward "components/playlist/target/target_output"; @@ -27,6 +28,8 @@ @forward "components/playlist/target/strm_output"; @forward "components/playlist/target/hdhomerun_output"; @forward "components/playlist/target/target_table"; +@forward "components/playlist/target/target_sort"; +@forward "components/playlist/target/target_rename"; @forward "components/playlist/playlist_mappings"; @forward "components/playlist/playlist_processing"; @forward "components/tag_list"; diff --git a/webui/scss/app/components/_popup_menu.scss b/webui/scss/app/components/_popup_menu.scss index 0c0998691..bcaf79830 100644 --- a/webui/scss/app/components/_popup_menu.scss +++ b/webui/scss/app/components/_popup_menu.scss @@ -9,4 +9,9 @@ box-shadow: var(--popup-shadow); z-index: 9999; + hr { + border: none; + height: 2px; + background-color: var(--border-color); + } } diff --git a/webui/scss/app/components/playlist/target/_target_common.scss b/webui/scss/app/components/playlist/target/_target_common.scss new file mode 100644 index 000000000..0085707c7 --- /dev/null +++ b/webui/scss/app/components/playlist/target/_target_common.scss @@ -0,0 +1,41 @@ +.tp__target-common { + display: flex; + justify-content: center; + flex-flow: column; + gap: var(--gap-small); + box-sizing: border-box; + + &__section { + display: flex; + flex-flow: column; + gap: var(--gap-small); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: var(--padding-micro) var(--padding-mini); + background-color: var(--table-card-background-color); + overflow: hidden; + + .tp__tag_list { + flex-flow: row nowrap; + } + + li { + overflow: hidden; + text-overflow: ellipsis; + } + + } + &__row { + flex-flow: row; + } + &__label { + font-weight: bold; + white-space: nowrap; + vertical-align: middle; + color: var(--modest-text-color); + + &:after { + content: ":"; + } + } +} diff --git a/webui/scss/app/components/playlist/target/_target_output.scss b/webui/scss/app/components/playlist/target/_target_output.scss index d3989f45f..556dda26f 100644 --- a/webui/scss/app/components/playlist/target/_target_output.scss +++ b/webui/scss/app/components/playlist/target/_target_output.scss @@ -11,48 +11,6 @@ } } -.tp__target-output__output { - display: flex; - justify-content: center; - flex-flow: column; - gap: var(--gap-small); - box-sizing: border-box; - - &__section { - display: flex; - flex-flow: column; - gap: var(--gap-small); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - padding: var(--padding-micro) var(--padding-mini); - background-color: var(--table-card-background-color); - overflow: hidden; - - .tp__tag_list { - flex-flow: row nowrap; - } - - li { - overflow: hidden; - text-overflow: ellipsis; - } - - } - &__row { - flex-flow: row; - } - &__label { - font-weight: bold; - white-space: nowrap; - vertical-align: middle; - color: var(--modest-text-color); - - &:after { - content: ":"; - } - } -} - .tp__target-output__xtream.tp__target-output__has_options .tp__collapse-panel__header { color: var(--output-xtream-color); } diff --git a/webui/scss/app/components/playlist/target/_target_rename.scss b/webui/scss/app/components/playlist/target/_target_rename.scss new file mode 100644 index 000000000..6feea1a57 --- /dev/null +++ b/webui/scss/app/components/playlist/target/_target_rename.scss @@ -0,0 +1,41 @@ +.tp__target-rename { + display: flex; + justify-content: center; + flex-flow: column; + gap: var(--gap-small); + box-sizing: border-box; + + &__section { + display: flex; + flex-flow: column; + gap: var(--gap-small); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: var(--padding-micro) var(--padding-mini); + background-color: var(--table-card-background-color); + overflow: hidden; + + .tp__tag_list { + flex-flow: row nowrap; + } + + li { + overflow: hidden; + text-overflow: ellipsis; + } + + } + &__row { + flex-flow: row; + } + &__label { + font-weight: bold; + white-space: nowrap; + vertical-align: middle; + color: var(--modest-text-color); + + &:after { + content: ":"; + } + } +} diff --git a/webui/scss/app/components/playlist/target/_target_sort.scss b/webui/scss/app/components/playlist/target/_target_sort.scss new file mode 100644 index 000000000..57f0f52fe --- /dev/null +++ b/webui/scss/app/components/playlist/target/_target_sort.scss @@ -0,0 +1,49 @@ +.tp__target-sort { + display: flex; + justify-content: center; + flex-flow: column; + gap: var(--gap-small); + box-sizing: border-box; + + h2 { + margin-top: 0; + } + + .tp__target-sort__sequence { + max-height: 300px; + overflow: auto; + } + + &__section { + display: flex; + flex-flow: column; + gap: var(--gap-small); + padding: var(--padding-micro) var(--padding-mini); + overflow: hidden; + //border: 1px solid var(--border-color); + //border-radius: var(--border-radius); + + .tp__tag_list { + flex-flow: row nowrap; + } + + li { + overflow: hidden; + text-overflow: ellipsis; + } + + } + &__row { + flex-flow: row; + } + &__label { + font-weight: bold; + white-space: nowrap; + vertical-align: middle; + color: var(--modest-text-color); + + &:after { + content: ":"; + } + } +} diff --git a/webui/src/app/components/playlist/target/hdhomerun_output.rs b/webui/src/app/components/playlist/target/hdhomerun_output.rs index d0aa64206..89ffec44c 100644 --- a/webui/src/app/components/playlist/target/hdhomerun_output.rs +++ b/webui/src/app/components/playlist/target/hdhomerun_output.rs @@ -11,17 +11,17 @@ pub struct HdHomeRunOutputProps { pub fn HdHomeRunOutput(props: &HdHomeRunOutputProps) -> Html { let translator = use_translation(); html! { -
-
- {translator.t("LABEL.DEVICE")} +
+
+ {translator.t("LABEL.DEVICE")} { props.output.device.clone() }
-
- {translator.t("LABEL.USERNAME")} +
+ {translator.t("LABEL.USERNAME")} { props.output.username.clone() }
-
- {translator.t("LABEL.USE_OUTPUT")} +
+ {translator.t("LABEL.USE_OUTPUT")} { props.output.use_output.map_or_else(String::new, |o| o.to_string()) }
diff --git a/webui/src/app/components/playlist/target/m3u_output.rs b/webui/src/app/components/playlist/target/m3u_output.rs index b30db54db..81032dede 100644 --- a/webui/src/app/components/playlist/target/m3u_output.rs +++ b/webui/src/app/components/playlist/target/m3u_output.rs @@ -27,12 +27,12 @@ pub fn M3uOutput(props: &M3uOutputProps) -> Html { html! { -
-
- {translator.t("LABEL.FILENAME")} +
+
+ {translator.t("LABEL.FILENAME")} { props.output.filename.as_ref().map(|f| html! {{ f }}) }
-
+
diff --git a/webui/src/app/components/playlist/target/mod.rs b/webui/src/app/components/playlist/target/mod.rs index 2ad18a0bf..63c083386 100644 --- a/webui/src/app/components/playlist/target/mod.rs +++ b/webui/src/app/components/playlist/target/mod.rs @@ -6,6 +6,7 @@ mod target_output; mod target_options; mod target_watch; mod target_sort; +mod target_rename; pub use self::xtream_output::*; pub use self::m3u_output::*; @@ -15,3 +16,5 @@ pub use self::target_output::*; pub use self::target_options::*; pub use self::target_watch::*; pub use self::target_sort::*; +pub use self::target_rename::*; + diff --git a/webui/src/app/components/playlist/target/strm_output.rs b/webui/src/app/components/playlist/target/strm_output.rs index 59d7bf036..facfcda6f 100644 --- a/webui/src/app/components/playlist/target/strm_output.rs +++ b/webui/src/app/components/playlist/target/strm_output.rs @@ -26,24 +26,24 @@ pub fn StrmOutput(props: &StrmOutputProps) -> Html { }; html! { -
-
- {translator.t("LABEL.DIRECTORY")} +
+
+ {translator.t("LABEL.DIRECTORY")} { props.output.directory.clone() }
-
- {translator.t("LABEL.USERNAME")} +
+ {translator.t("LABEL.USERNAME")} { props.output.username.as_ref().map(|f| html! {{ f }}) }
-
+
{ match props.output.strm_props.as_ref() { None => html! {}, Some(props) => html! { -
- {translator.t("LABEL.PROPERTIES")} +
+ {translator.t("LABEL.PROPERTIES")}
    { props.iter().map(|p| html! {
  • {p}
  • }).collect::() }
diff --git a/webui/src/app/components/playlist/target/target_rename.rs b/webui/src/app/components/playlist/target/target_rename.rs new file mode 100644 index 000000000..a119fd034 --- /dev/null +++ b/webui/src/app/components/playlist/target/target_rename.rs @@ -0,0 +1,22 @@ +use shared::model::{ConfigTargetDto}; +use std::rc::Rc; +use yew::prelude::*; +use yew_i18n::use_translation; + +#[derive(Properties, Clone, PartialEq, Debug)] +pub struct TargetRenameProps { + pub target: Rc, +} + +#[function_component] +pub fn TargetRename(props: &TargetRenameProps) -> Html { + let translate = use_translation(); + match props.target.sort.as_ref() { + None => html! {}, + Some(sort) => html! { +
+ {"Sort"} +
+ } + } +} \ No newline at end of file diff --git a/webui/src/app/components/playlist/target/target_sort.rs b/webui/src/app/components/playlist/target/target_sort.rs index fdef130fd..15133cfab 100644 --- a/webui/src/app/components/playlist/target/target_sort.rs +++ b/webui/src/app/components/playlist/target/target_sort.rs @@ -1,4 +1,5 @@ -use shared::model::{ConfigTargetDto}; +use crate::app::components::{convert_bool_to_chip_style, Card, Chip}; +use shared::model::{ConfigTargetDto, ItemField, SortOrder}; use std::rc::Rc; use yew::prelude::*; use yew_i18n::use_translation; @@ -10,13 +11,85 @@ pub struct TargetSortProps { #[function_component] pub fn TargetSort(props: &TargetSortProps) -> Html { - let translate = use_translation(); - match props.target.sort.as_ref() { + let translator = use_translation(); + + let sort = match props.target.sort.as_ref() { + Some(s) => s, + None => return html! {}, + }; + + let groups_html = match sort.groups.as_ref() { + Some(groups) => { + match groups.sequence.as_ref() { + Some(seq) => html! { + +

{translator.t("LABEL.GROUPS")}

+
+ {translator.t("LABEL.ORDER")} + { groups.order.to_string() } +
+
+ {translator.t("LABEL.SEQUENCE")} + +
    + { for seq.iter().map(|p| html! {
  • {p}
  • }) } +
+
+
+
+ }, + None => html! {}, + } + }, None => html! {}, - Some(watch) => html! { -
- {"Sort"} + }; + + html! { +
+

{translator.t("LABEL.SORT_SETTINGS")}

+
+
- } + { groups_html } +
} -} \ No newline at end of file +} + +//
+//
+// {translator.t("LABEL.USERNAME")} +// { props.output.username.clone() } +//
+//
+// {translator.t("LABEL.USE_OUTPUT")} +// { props.output.use_output.map_or_else(String::new, |o| o.to_string()) } +//
+//
+ +// pub match_as_ascii: bool, +// pub groups: Option, +// pub channels: Option>, + +// +// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +// #[serde(deny_unknown_fields)] +// pub struct ConfigSortGroupDto { +// pub order: SortOrder, +// #[serde(default, skip_serializing_if = "Option::is_none")] +// pub sequence: Option>, +// #[serde(skip)] +// pub t_sequence: Option>, +// } + +// pub struct ConfigSortChannelDto { +// // channel field +// pub field: ItemField, +// // match against group title +// pub group_pattern: String, +// pub order: SortOrder, +// #[serde(default, skip_serializing_if = "Option::is_none")] +// pub sequence: Option>, +// #[serde(skip)] +// pub t_sequence: Option>, +// } diff --git a/webui/src/app/components/playlist/target/target_watch.rs b/webui/src/app/components/playlist/target/target_watch.rs index d96de0efc..03b984c41 100644 --- a/webui/src/app/components/playlist/target/target_watch.rs +++ b/webui/src/app/components/playlist/target/target_watch.rs @@ -1,7 +1,6 @@ use shared::model::{ConfigTargetDto}; use std::rc::Rc; use yew::prelude::*; -use yew_i18n::use_translation; #[derive(Properties, Clone, PartialEq, Debug)] pub struct TargetWatchProps { @@ -10,7 +9,6 @@ pub struct TargetWatchProps { #[function_component] pub fn TargetWatch(props: &TargetWatchProps) -> Html { - let translate = use_translation(); match props.target.watch.as_ref() { None => html! {}, Some(watch) => html! { diff --git a/webui/src/app/components/playlist/target/xtream_output.rs b/webui/src/app/components/playlist/target/xtream_output.rs index cfe9d945f..ee9e6fd2f 100644 --- a/webui/src/app/components/playlist/target/xtream_output.rs +++ b/webui/src/app/components/playlist/target/xtream_output.rs @@ -40,26 +40,15 @@ pub fn XtreamOutput(props: &XtreamOutputProps) -> Html { }; html! { -
-
- {translator.t("LABEL.SKIP_DIRECT_SOURCE")} +
+
+ {translator.t("LABEL.SKIP_DIRECT_SOURCE")}
-
- {translator.t("LABEL.RESOLVE")} +
+ {translator.t("LABEL.RESOLVE")}
} } - -/* - -
- - #[serde(default = "default_as_true")] - #[serde(default, skip_serializing_if = "Option::is_none")] - pub trakt: Option, - {"xc"} - }, - */ \ No newline at end of file diff --git a/webui/src/app/components/playlist/target_table.rs b/webui/src/app/components/playlist/target_table.rs index 58e9a11d3..e619e1dc7 100644 --- a/webui/src/app/components/playlist/target_table.rs +++ b/webui/src/app/components/playlist/target_table.rs @@ -1,12 +1,13 @@ use std::fmt::Display; use crate::app::components::popup_menu::PopupMenu; use crate::app::components::reveal_content::RevealContent; -use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, PlaylistMappings, PlaylistProcessing, Table, TableDefinition, TargetOptions, TargetOutput, TargetSort, TargetWatch}; +use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, PlaylistMappings, PlaylistProcessing, Table, TableDefinition, TargetOptions, TargetOutput, TargetRename, TargetSort, TargetWatch}; use crate::hooks::use_service_context; use shared::model::{ConfigTargetDto}; use std::future; use std::rc::Rc; use std::str::FromStr; +use log::info; use yew::platform::spawn_local; use yew::prelude::*; use yew::suspense::use_future; @@ -97,9 +98,9 @@ pub fn TargetTable() -> Html { 2 => html! { &dto.name.to_string() }, 3 => html! { }, 4 => html! { }, - 5 => dto.sort.as_ref().map_or_else(|| html! {}, |s| html! { {format!("{s:?}")} }), + 5 => dto.sort.as_ref().map_or_else(|| html! {}, |s| html! { }), 6 => html! { &dto.filter.clone() }, - 7 => html! { }, + 7 => dto.rename.as_ref().map_or_else(|| html! {}, |r| html! { }), 8 => html! { }, 9 => html! { }, 10 => html! { }, @@ -146,14 +147,27 @@ pub fn TargetTable() -> Html { }); } - let handle_menu_edit = { + let handle_menu_click = { let popup_is_open_state = popup_is_open.clone(); let confirm = dialog.clone(); let translate = translate.clone(); + let services_ctx = services.clone(); + let selected_dto = selected_dto.clone(); Callback::from(move |name:String| { if let Ok(action) = TableAction::from_str(&name) { match action { TableAction::Edit => {} + TableAction::Refresh => { + let services_ctx = services_ctx.clone(); + let dto_name = selected_dto.as_ref().map_or_else(String::new, |d| d.name.to_string()); + spawn_local(async move { + let targets = vec![dto_name.as_str()]; + match services_ctx.playlist.update_targets(&targets).await { + true => { info!("Ok"); } + false => { info!("not ok"); } + } + }); + } TableAction::Delete => { let confirm = confirm.clone(); let translator = translate.clone(); @@ -178,8 +192,10 @@ pub fn TargetTable() -> Html { <> definition={(*table_definition).as_ref().unwrap().clone()} /> - - + + +
+
} @@ -197,6 +213,7 @@ pub fn TargetTable() -> Html { #[derive(Debug, Clone, Eq, PartialEq)] enum TableAction { Edit, + Refresh, Delete, } @@ -204,6 +221,7 @@ impl Display for TableAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", match self { Self::Edit => "edit", + Self::Refresh => "refresh", Self::Delete => "delete", }) } @@ -215,6 +233,8 @@ impl FromStr for TableAction { fn from_str(s: &str) -> Result { if s.eq("edit") { Ok(Self::Edit) + } else if s.eq("refresh") { + Ok(Self::Refresh) } else if s.eq("delete") { Ok(Self::Delete) } else { diff --git a/webui/src/app/components/reveal_content.rs b/webui/src/app/components/reveal_content.rs index e8f01ddcc..45111d2ba 100644 --- a/webui/src/app/components/reveal_content.rs +++ b/webui/src/app/components/reveal_content.rs @@ -28,7 +28,7 @@ pub fn RevealContent(props: &RevealContentProps) -> Html { let actions = actions.clone(); let dlg = dialog.clone(); spawn_local(async move { - let result = dlg.content(content, actions).await; + let _result = dlg.content(content, actions).await; }); }) }; diff --git a/webui/src/hooks/use_service_context.rs b/webui/src/hooks/use_service_context.rs index c436bccd2..a4d09bbd6 100644 --- a/webui/src/hooks/use_service_context.rs +++ b/webui/src/hooks/use_service_context.rs @@ -1,12 +1,13 @@ use std::rc::Rc; use yew::prelude::*; use crate::model::WebConfig; -use crate::services::{AuthService, ConfigService, StatusService}; +use crate::services::{AuthService, ConfigService, PlaylistService, StatusService}; pub struct Services { pub auth: Rc, pub config: Rc, pub status: Rc, + pub playlist: Rc, } impl Services { @@ -14,10 +15,12 @@ impl Services { let auth = Rc::new(AuthService::new()); let config = Rc::new(ConfigService::new(config)); let status = Rc::new(StatusService::new()); + let playlist = Rc::new(PlaylistService::new()); Self { auth, config, - status + status, + playlist } } } diff --git a/webui/src/services/mod.rs b/webui/src/services/mod.rs index 1adead732..16899c4c3 100644 --- a/webui/src/services/mod.rs +++ b/webui/src/services/mod.rs @@ -3,9 +3,11 @@ mod config_service; mod requests; mod status_service; mod dialog_service; +mod playlist_service; pub use self::auth_service::*; pub use self::config_service::*; pub use self::requests::*; pub use self::status_service::*; pub use self::dialog_service::*; +pub use self::playlist_service::*; diff --git a/webui/src/services/playlist_service.rs b/webui/src/services/playlist_service.rs new file mode 100644 index 000000000..ae0e24a0a --- /dev/null +++ b/webui/src/services/playlist_service.rs @@ -0,0 +1,22 @@ +use crate::services::{request_post}; +use std::rc::Rc; +use log::error; + +const TARGET_UPDATE_API_PATH: &str = "/api/v1/playlist/update"; + +pub struct PlaylistService { +} + +impl PlaylistService { + pub fn new() -> Self { + Self { + } + } + + pub async fn update_targets(&self, targets: &[&str]) -> bool { + request_post::<&[&str], ()>(TARGET_UPDATE_API_PATH, targets).await.map_or_else(|err| { + error!("{err}"); + false + }, |_| true) + } +} diff --git a/webui/src/services/requests.rs b/webui/src/services/requests.rs index 250114ff0..a677e0b91 100644 --- a/webui/src/services/requests.rs +++ b/webui/src/services/requests.rs @@ -44,16 +44,18 @@ where request = request.header("Authorization", format!("Bearer {token}").as_str()); } match request.send().await { - Ok(response) => { match response.status() { 200 => { - let data: Result = response.json::().await; - if let Ok(data) = data { - // debug!("Response: {:?}", data); - Ok(data) + if std::any::TypeId::of::() == std::any::TypeId::of::<()>() { + Ok(serde_json::from_str("null").unwrap()) // `T = ()` valid } else { - Err(Error::DeserializeError) + let data: Result = response.json::().await; + if let Ok(data) = data { + Ok(data) + } else { + Err(Error::DeserializeError) + } } }, 401 => Err(Error::Unauthorized),