diff --git a/backend/src/processing/processor/xtream_series.rs b/backend/src/processing/processor/xtream_series.rs index 044d71e5d..5aa7da0bf 100644 --- a/backend/src/processing/processor/xtream_series.rs +++ b/backend/src/processing/processor/xtream_series.rs @@ -741,18 +741,38 @@ pub async fn update_series_metadata( .await .map_err(|e| shared::error::info_err!("{e}"))?; - if !content.is_empty() { - if let Ok(mut json_value) = serde_json::from_str::(&content) { - if let Some(info) = json_value.get_mut("info").and_then(|v| v.as_object_mut()) { - crate::model::normalize_release_date(info); - } - - if let Ok(info) = serde_json::from_value::(json_value) { - if let Some(existing) = &existing_item { - props = Some(SeriesStreamProperties::from_info(&info, existing)); - fetched_new = true; - properties_updated = true; + if content.is_empty() { + debug!("Series {display_id}: provider returned empty content for info fetch"); + } else { + let canonical_series_id = + existing_item.as_ref().map_or(series_id, PlaylistEntry::get_virtual_id); + match serde_json::from_str::(&content) { + Ok(mut json_value) => { + if let Some(info) = json_value.get_mut("info").and_then(|v| v.as_object_mut()) { + crate::model::normalize_release_date(info); } + + match serde_json::from_value::(json_value) { + Ok(info) => { + debug!("Series {display_id}: building props from info using canonical series id {canonical_series_id}"); + props = Some(SeriesStreamProperties::from_info_without_existing( + &info, + canonical_series_id, + )); + fetched_new = true; + properties_updated = true; + } + Err(e) => { + return Err(shared::error::info_err!( + "Series {display_id}: failed to parse XtreamSeriesInfo: {e}. Raw response: {content}" + )); + } + } + } + Err(e) => { + return Err(shared::error::info_err!( + "Series {display_id}: failed to parse provider response as JSON: {e}. Raw response: {content}" + )); } } } diff --git a/frontend/src/app/components/date_input.rs b/frontend/src/app/components/date_input.rs index 8b41ba025..f949c3a8c 100644 --- a/frontend/src/app/components/date_input.rs +++ b/frontend/src/app/components/date_input.rs @@ -2,6 +2,12 @@ use crate::app::components::FieldLabel; use web_sys::HtmlInputElement; use yew::{classes, component, html, use_effect_with, Callback, Html, NodeRef, Properties, TargetCast}; +pub(crate) fn format_date_input_value(value: Option) -> String { + value + .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0)) + .map_or_else(String::new, |date| date.format("%Y-%m-%d").to_string()) +} + #[derive(Properties, Clone, PartialEq, Debug)] pub struct DateInputProps { #[prop_or_default] @@ -47,13 +53,7 @@ pub(crate) fn DateInputBase(props: &DateInputBaseProps) -> Html { let value = props.value; use_effect_with(value, move |val| { if let Some(input) = local_ref.cast::() { - if let Some(ts) = val { - if let Some(date) = chrono::DateTime::from_timestamp(*ts, 0) { - input.set_value(&date.format("%Y-%m-%d").to_string()); - } - } else { - input.set_value(""); - } + input.set_value(&format_date_input_value(*val)); } || () }); diff --git a/frontend/src/app/components/mod.rs b/frontend/src/app/components/mod.rs index 2c058985b..0111cedf6 100644 --- a/frontend/src/app/components/mod.rs +++ b/frontend/src/app/components/mod.rs @@ -67,9 +67,9 @@ mod setup; pub(crate) use self::{ accordion::*, accordion_panel::*, authentication::*, breadcrumbs::*, card::*, cell_value::*, chip::*, - cluster_flags_input::*, collapse_panel::*, csv_table::*, custom_dialog::*, dashboard::*, date_input_action::*, - drop_down_icon_button::*, field_explanation::*, field_id::*, filter::*, hide_content::*, home::*, icon_button::*, - key_value_editor::*, loading_screen::*, login::*, no_access::*, no_content::*, panel::*, + cluster_flags_input::*, collapse_panel::*, csv_table::*, custom_dialog::*, dashboard::*, date_input::*, + date_input_action::*, drop_down_icon_button::*, field_explanation::*, field_id::*, filter::*, hide_content::*, + home::*, icon_button::*, key_value_editor::*, loading_screen::*, login::*, no_access::*, no_content::*, panel::*, particle_flow_background::*, playlist::*, radio_button_group::*, rbac::*, reveal_content::*, role_based_content::*, search::*, select::*, setup::*, sidebar::*, source_editor::*, svg_icon::*, table::*, tabset::*, tag_list::*, text_button::*, textarea::*, title_card::*, toastr::*, toggle_switch::*, userlist::*, websocket_status::*, diff --git a/frontend/src/app/components/source_editor/alias_item_form.rs b/frontend/src/app/components/source_editor/alias_item_form.rs index 4d7eade31..8998e559c 100644 --- a/frontend/src/app/components/source_editor/alias_item_form.rs +++ b/frontend/src/app/components/source_editor/alias_item_form.rs @@ -1,5 +1,6 @@ use crate::{ - app::components::{Card, TextButton, ToolAction}, + app::components::{format_date_input_value, Card, TextButton, ToolAction}, + config_field, config_field_bool, config_field_custom, config_field_optional, config_field_optional_hide, edit_field_bool, edit_field_exp_date, edit_field_number_i16, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer, hooks::use_service_context, @@ -43,6 +44,8 @@ pub struct AliasItemFormProps { pub input_type: InputType, #[prop_or_default] pub initial: Option, + #[prop_or(false)] + pub readonly: bool, } #[component] @@ -168,16 +171,34 @@ pub fn AliasItemForm(props: &AliasItemFormProps) -> Html { html! { - { edit_field_bool!(form_state, translate.t(LABEL_ENABLED), enabled, AliasFormAction::Enabled) } - { edit_field_text!(form_state, translate.t(LABEL_ALIAS_NAME), name, AliasFormAction::Name) } - { edit_field_text!(form_state, translate.t(LABEL_URL), url, AliasFormAction::Url) } -
- { edit_field_text_option!(form_state, translate.t(LABEL_USERNAME), username, AliasFormAction::Username) } - { edit_field_text_option!(form_state, translate.t(LABEL_PASSWORD), password, AliasFormAction::Password, true) } - { edit_field_number_i16!(form_state, translate.t(LABEL_PRIORITY), priority, AliasFormAction::Priority) } - { edit_field_number_u16!(form_state, translate.t(LABEL_MAX_CONNECTIONS), max_connections, AliasFormAction::MaxConnections) } -
- { edit_field_exp_date!(form_state, translate.t(LABEL_EXP_DATE), exp_date, AliasFormAction::ExpDate, exp_date_tool_action) } + if props.readonly { + { config_field_bool!(form_state.form, translate.t(LABEL_ENABLED), enabled) } + { config_field!(form_state.form, translate.t(LABEL_ALIAS_NAME), name) } + { config_field!(form_state.form, translate.t(LABEL_URL), url) } +
+ { config_field_optional!(form_state.form, translate.t(LABEL_USERNAME), username) } + { config_field_optional_hide!(form_state.form, translate.t(LABEL_PASSWORD), password) } + { config_field_custom!(translate.t(LABEL_PRIORITY), form_state.form.priority.to_string()) } + { config_field_custom!(translate.t(LABEL_MAX_CONNECTIONS), form_state.form.max_connections.to_string()) } +
+ { config_field_custom!( + translate.t(LABEL_EXP_DATE), + format_date_input_value(form_state.form.exp_date) + ) } + } else { + <> + { edit_field_bool!(form_state, translate.t(LABEL_ENABLED), enabled, AliasFormAction::Enabled) } + { edit_field_text!(form_state, translate.t(LABEL_ALIAS_NAME), name, AliasFormAction::Name) } + { edit_field_text!(form_state, translate.t(LABEL_URL), url, AliasFormAction::Url) } +
+ { edit_field_text_option!(form_state, translate.t(LABEL_USERNAME), username, AliasFormAction::Username) } + { edit_field_text_option!(form_state, translate.t(LABEL_PASSWORD), password, AliasFormAction::Password, true) } + { edit_field_number_i16!(form_state, translate.t(LABEL_PRIORITY), priority, AliasFormAction::Priority) } + { edit_field_number_u16!(form_state, translate.t(LABEL_MAX_CONNECTIONS), max_connections, AliasFormAction::MaxConnections) } +
+ { edit_field_exp_date!(form_state, translate.t(LABEL_EXP_DATE), exp_date, AliasFormAction::ExpDate, exp_date_tool_action) } + + }
Html { title={translate.t("LABEL.CANCEL")} onclick={handle_cancel} /> - + if !props.readonly { + + }
} diff --git a/frontend/src/app/components/source_editor/editor_form.rs b/frontend/src/app/components/source_editor/editor_form.rs index 249372302..a9ff75318 100644 --- a/frontend/src/app/components/source_editor/editor_form.rs +++ b/frontend/src/app/components/source_editor/editor_form.rs @@ -25,9 +25,9 @@ pub fn SourceEditorForm() -> Html { { if let EditMode::Active(block) = &*source_editor_ctx.edit_mode { match &block.instance { - BlockInstance::Input(input) => html! { }, - BlockInstance::Target(target) => html! { }, - BlockInstance::Output(output) => html! { }, + BlockInstance::Input(input) => html! { }, + BlockInstance::Target(target) => html! { }, + BlockInstance::Output(output) => html! { }, } } else { html!{} diff --git a/frontend/src/app/components/source_editor/editor_model.rs b/frontend/src/app/components/source_editor/editor_model.rs index 2da5c9eb0..207cf0c3f 100644 --- a/frontend/src/app/components/source_editor/editor_model.rs +++ b/frontend/src/app/components/source_editor/editor_model.rs @@ -157,6 +157,7 @@ pub enum EditMode { pub struct SourceEditorContext { pub on_form_change: Callback<(BlockId, BlockInstance)>, pub edit_mode: UseStateHandle, + pub allow_write: bool, } impl PartialEq for SourceEditorContext { diff --git a/frontend/src/app/components/source_editor/editor_view.rs b/frontend/src/app/components/source_editor/editor_view.rs index 867736c37..d7019c1d5 100644 --- a/frontend/src/app/components/source_editor/editor_view.rs +++ b/frontend/src/app/components/source_editor/editor_view.rs @@ -726,6 +726,9 @@ pub fn SourceEditor(props: &SourceEditorProps) -> Html { let editor_state_ref = editor_state_ref.clone(); let force_update = force_update.clone(); Callback::from(move |from_id: BlockId| { + if !can_write_sources { + return; + } let pending_line = { let editor_state = editor_state_ref.borrow(); if let Some(block) = editor_state.get_block(from_id) { @@ -755,6 +758,9 @@ pub fn SourceEditor(props: &SourceEditorProps) -> Html { let force_update = force_update.clone(); let emit_sources_change = emit_sources_change.clone(); Callback::from(move |to_id: BlockId| { + if !can_write_sources { + return; + } let mut changed = false; let pending_connection = editor_state_ref.borrow().pending_connection; if let Some(from_id) = pending_connection { @@ -796,6 +802,9 @@ pub fn SourceEditor(props: &SourceEditorProps) -> Html { let cursor_grabbing = cursor_grabbing.clone(); Callback::from(move |(block_id, e): (BlockId, MouseEvent)| { + if !can_write_sources { + return; + } e.prevent_default(); e.stop_propagation(); @@ -1340,6 +1349,9 @@ pub fn SourceEditor(props: &SourceEditorProps) -> Html { let editor_state_ref = editor_state_ref.clone(); let emit_sources_change = emit_sources_change.clone(); Callback::<(BlockId, BlockInstance)>::from(move |(block_id, instance): (BlockId, BlockInstance)| { + if !can_write_sources { + return; + } let mut editor_state = editor_state_ref.borrow_mut(); if let Some(block) = editor_state.get_block_mut(block_id) { block.instance = match instance { @@ -1362,9 +1374,6 @@ pub fn SourceEditor(props: &SourceEditorProps) -> Html { let edit_mode_set = edit_mode.clone(); let editor_state_ref = editor_state_ref.clone(); Callback::from(move |block_id: BlockId| { - if !can_write_sources { - return; - } let mut editor_state = editor_state_ref.borrow_mut(); if let Some(block) = editor_state.get_block(block_id) { edit_mode_set.set(EditMode::Active(block.clone())); @@ -1373,7 +1382,11 @@ pub fn SourceEditor(props: &SourceEditorProps) -> Html { }) }; - let editor_context = SourceEditorContext { on_form_change: form_changed, edit_mode: edit_mode.clone() }; + let editor_context = SourceEditorContext { + on_form_change: form_changed, + edit_mode: edit_mode.clone(), + allow_write: can_write_sources, + }; let edited_block_id = match *edit_mode { EditMode::Inactive => 0, diff --git a/frontend/src/app/components/source_editor/epg_source_item_form.rs b/frontend/src/app/components/source_editor/epg_source_item_form.rs index 1f795de33..07a30c18a 100644 --- a/frontend/src/app/components/source_editor/epg_source_item_form.rs +++ b/frontend/src/app/components/source_editor/epg_source_item_form.rs @@ -1,6 +1,7 @@ use crate::{ app::components::{Card, TextButton}, - edit_field_bool, edit_field_number_i16, edit_field_text, generate_form_reducer, + config_field, config_field_bool, config_field_custom, edit_field_bool, edit_field_number_i16, edit_field_text, + generate_form_reducer, i18n::use_translation, }; use shared::model::EpgSourceDto; @@ -26,6 +27,8 @@ pub struct EpgSourceItemFormProps { pub on_cancel: Callback<()>, #[prop_or_default] pub initial: Option, + #[prop_or(false)] + pub readonly: bool, } #[component] @@ -61,9 +64,15 @@ pub fn EpgSourceItemForm(props: &EpgSourceItemFormProps) -> Html { html! { - { edit_field_text!(form_state, translate.t(LABEL_EPG_SOURCE_URL), url, EpgSourceFormAction::Url) } - { edit_field_number_i16!(form_state, translate.t(LABEL_EPG_PRIORITY), priority, EpgSourceFormAction::Priority) } - { edit_field_bool!(form_state, translate.t(LABEL_EPG_LOGO_OVERRIDE), logo_override, EpgSourceFormAction::LogoOverride) } + if props.readonly { + { config_field!(form_state.form, translate.t(LABEL_EPG_SOURCE_URL), url) } + { config_field_custom!(translate.t(LABEL_EPG_PRIORITY), form_state.form.priority.to_string()) } + { config_field_bool!(form_state.form, translate.t(LABEL_EPG_LOGO_OVERRIDE), logo_override) } + } else { + { edit_field_text!(form_state, translate.t(LABEL_EPG_SOURCE_URL), url, EpgSourceFormAction::Url) } + { edit_field_number_i16!(form_state, translate.t(LABEL_EPG_PRIORITY), priority, EpgSourceFormAction::Priority) } + { edit_field_bool!(form_state, translate.t(LABEL_EPG_LOGO_OVERRIDE), logo_override, EpgSourceFormAction::LogoOverride) } + }
Html { title={translate.t("LABEL.CANCEL")} onclick={handle_cancel} /> - + if !props.readonly { + + }
} diff --git a/frontend/src/app/components/source_editor/input_form.rs b/frontend/src/app/components/source_editor/input_form.rs index dfcd1d6fa..6a4f35a40 100644 --- a/frontend/src/app/components/source_editor/input_form.rs +++ b/frontend/src/app/components/source_editor/input_form.rs @@ -7,7 +7,8 @@ use crate::{ }, ConfigContext, }, - config_field_child, edit_field_bool, edit_field_exp_date, edit_field_number_i16, edit_field_number_u16, + config_field, config_field_bool, config_field_child, config_field_custom, config_field_optional, + config_field_optional_hide, edit_field_bool, edit_field_exp_date, edit_field_number_i16, edit_field_number_u16, edit_field_number_u32, edit_field_text, edit_field_text_option, generate_form_reducer, hooks::use_service_context, html_if, @@ -217,6 +218,8 @@ pub struct ConfigInputViewProps { #[prop_or_default] pub(crate) block_id: Option, pub(crate) input: Option>, + #[prop_or(true)] + pub(crate) allow_write: bool, #[prop_or_default] pub(crate) on_apply: Option>, #[prop_or_default] @@ -268,6 +271,7 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { let show_provider_form_state = use_state(|| false); let edit_alias = use_state(|| None::); let edit_provider = use_state(|| None::); + let edit_epg_source = use_state(|| None::); let exp_date_loading = use_state(|| false); let exp_date_request_in_flight = use_mut_ref(|| false); let exp_date_request_token = use_mut_ref(|| 0_u64); @@ -365,9 +369,21 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { let handle_add_epg_item = { let epg_sources = epg_sources_state.clone(); let show_epg_form = show_epg_form_state.clone(); + let edit_epg_source = edit_epg_source.clone(); Callback::from(move |source: EpgSourceDto| { let mut sources = (*epg_sources).clone(); - sources.push(source); + if let Some(existing) = edit_epg_source.as_ref() { + if let Some(position) = sources.iter().position(|item| item == existing) { + if let Some(slot) = sources.get_mut(position) { + *slot = source; + } + } else { + sources.push(source); + } + edit_epg_source.set(None); + } else { + sources.push(source); + } epg_sources.set(sources); show_epg_form.set(false); }) @@ -375,15 +391,36 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { let handle_close_add_epg_item = { let show_epg_form = show_epg_form_state.clone(); + let edit_epg_source = edit_epg_source.clone(); Callback::from(move |_| { show_epg_form.set(false); + edit_epg_source.set(None); }) }; let handle_show_add_epg_item = { let show_epg_form = show_epg_form_state.clone(); + let edit_epg_source = edit_epg_source.clone(); Callback::from(move |_| { show_epg_form.set(true); + edit_epg_source.set(None); + }) + }; + + let handle_edit_epg_source = { + let epg_list = epg_sources_state.clone(); + let show_epg_form = show_epg_form_state.clone(); + let edit_epg_source = edit_epg_source.clone(); + Callback::from(move |(idx, e): (String, MouseEvent)| { + e.prevent_default(); + e.stop_propagation(); + if let Ok(index) = idx.parse::() { + let items = (*epg_list).clone(); + if let Some(item) = items.get(index).cloned() { + edit_epg_source.set(Some(item)); + show_epg_form.set(true); + } + } }) }; @@ -589,6 +626,53 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { let xtream_input = input_form_state.form.input_type.is_xtream(); let render_options = || { + if !props.allow_write { + return html! { + + { html_if!(xtream_input, { + <> + +
+ { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_SKIP_LIVE), xtream_skip_live) } + { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_SKIP_VOD), xtream_skip_vod) } + { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_SKIP_SERIES), xtream_skip_series) } +
+
+ +
+ { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_LIVE_STREAM_USE_PREFIX), xtream_live_stream_use_prefix) } + { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_LIVE_STREAM_WITHOUT_EXTENSION), xtream_live_stream_without_extension) } +
+
+ +
+ { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_SKIP_VOD), resolve_vod) } + { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_SKIP_SERIES), resolve_series) } +
+
+ { config_field_custom!(translate.t(LABEL_RESOLVE_DELAY_SEC), input_options_state.form.resolve_delay.to_string()) } +
+ { config_field_bool!(input_options_state.form, translate.t(LABEL_RESOLVE_BACKGROUND), resolve_background) } +
+ +
+ { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_SKIP_LIVE), probe_live) } + { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_SKIP_VOD), probe_vod) } + { config_field_bool!(input_options_state.form, translate.t(LABEL_XTREAM_SKIP_SERIES), probe_series) } +
+
+ { config_field_custom!(translate.t(LABEL_PROBE_DELAY_SEC), input_options_state.form.probe_delay.to_string()) } + { config_field_custom!(translate.t(LABEL_PROBE_LIVE_INTERVAL_HOURS), input_options_state.form.probe_live_interval_hours.to_string()) } +
+
+ + })} + + { config_field_bool!(input_options_state.form, translate.t(LABEL_RESOLVE_TMDB), resolve_tmdb) } + +
+ }; + } html! { { html_if!(xtream_input, { @@ -647,6 +731,29 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { let live_source_options = cluster_source_options(staged_input_state.form.live_source); let vod_source_options = cluster_source_options(staged_input_state.form.vod_source); let series_source_options = cluster_source_options(staged_input_state.form.series_source); + if !props.allow_write { + return html! { + + { config_field_bool!(staged_input_state.form, translate.t(LABEL_ENABLED), enabled) } + { config_field!(staged_input_state.form, translate.t(LABEL_URL), url) } +
+ { config_field_optional!(staged_input_state.form, translate.t(LABEL_USERNAME), username) } + { config_field_optional_hide!(staged_input_state.form, translate.t(LABEL_PASSWORD), password) } + { config_field_custom!(translate.t(LABEL_FETCH_METHOD), staged_input_state.form.method.to_string()) } + { config_field_custom!(translate.t(LABEL_INPUT_TYPE), staged_input_state.form.input_type.to_string()) } +
+ { + html_if!(show_cluster_sources, { +
+ { config_field_custom!(translate.t(LABEL_LIVE_SOURCE), staged_input_state.form.live_source.map_or_else(String::new, |source| source.to_string())) } + { config_field_custom!(translate.t(LABEL_VOD_SOURCE), staged_input_state.form.vod_source.map_or_else(String::new, |source| source.to_string())) } + { config_field_custom!(translate.t(LABEL_SERIES_SOURCE), staged_input_state.form.series_source.map_or_else(String::new, |source| source.to_string())) } +
+ }) + } +
+ }; + } html! { { edit_field_bool!(staged_input_state, translate.t(LABEL_ENABLED), enabled, StagedInputFormAction::Enabled) } @@ -839,6 +946,31 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { None }; + if !props.allow_write { + return html! { + +
+ { config_field!(input_form_state.form, translate.t(LABEL_NAME), name) } + { config_field_bool!(input_form_state.form, translate.t(LABEL_ENABLED), enabled) } +
+ { html_if!(!library_input, { + <> + { config_field!(input_form_state.form, translate.t(LABEL_URL), url) } +
+ { config_field_optional!(input_form_state.form, translate.t(LABEL_USERNAME), username) } + { config_field_optional_hide!(input_form_state.form, translate.t(LABEL_PASSWORD), password) } + { config_field_custom!(translate.t(LABEL_MAX_CONNECTIONS), input_form_state.form.max_connections.to_string()) } + { config_field_custom!(translate.t(LABEL_PRIORITY), input_form_state.form.priority.to_string()) } + { config_field_custom!(translate.t(LABEL_EXP_DATE), input_form_state.form.exp_date.map_or_else(String::new, |exp_date| exp_date.to_string())) } + { config_field_optional!(input_form_state.form, translate.t(LABEL_CACHE_DURATION), cache_duration) } + { config_field_custom!(translate.t(LABEL_FETCH_METHOD), input_form_state.form.method.to_string()) } +
+ { config_field_optional!(input_form_state.form, translate.t(LABEL_PERSIST), persist) } + + })} +
+ }; + } html! {
@@ -889,6 +1021,7 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { initial={(*edit_alias).clone()} on_submit={handle_add_alias_item} on_cancel={handle_close_add_alias_item} + readonly={!props.allow_write} /> } else { { config_field_child!(translate.t(LABEL_ALIASES), "INPUT_FORM.ALIASES", { @@ -902,34 +1035,36 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { html! {
- if idx > 0 { + if props.allow_write && idx > 0 { - } else if alias_count > 2 { + } else if props.allow_write && alias_count > 2 { } - if idx + 1 < alias_count { + if props.allow_write && idx + 1 < alias_count { - } else if alias_count > 2 { + } else if props.allow_write && alias_count > 2 { } - + if props.allow_write { + + }
@@ -947,15 +1082,17 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { }) }
-
- -
+ if props.allow_write { +
+ +
+ }
} })} @@ -976,6 +1113,7 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { initial={(*edit_provider).clone()} on_submit={handle_add_provider_item.clone()} on_cancel={handle_close_add_provider_item.clone()} + readonly={!props.allow_write} /> } else { { config_field_child!(translate.t(LABEL_PROVIDERS), "INPUT_FORM.PROVIDERS", { @@ -988,16 +1126,18 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { html! {
- + if props.allow_write { + + }
@@ -1009,15 +1149,17 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { }) }
-
- -
+ if props.allow_write { +
+ +
+ }
} })} @@ -1030,6 +1172,7 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { let headers = headers_state.clone(); let epg_sources = epg_sources_state.clone(); let show_epg_form = show_epg_form_state.clone(); + let edit_epg_source = edit_epg_source.clone(); html! { @@ -1037,6 +1180,8 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { } else { // Headers Section @@ -1045,7 +1190,7 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { html! { | { @@ -1066,25 +1211,42 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { for (*epg_sources_list).iter().enumerate().map(|(idx, source)| { html! {
- +
+ + if props.allow_write { + + } +
- {&source.url} + + {&source.url} + {" ("} + {source.priority} + {", "} + {if source.logo_override { "logo_override" } else { "no_logo_override" }} + {")"} +
} }) }
- + if props.allow_write { + + } } }) @@ -1222,10 +1384,12 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html { icon="Cancel" title={ translate.t("LABEL.CANCEL")} onclick={handle_cancel}> - + if props.allow_write { + + }
{ render_sidebar() } diff --git a/frontend/src/app/components/source_editor/output_form.rs b/frontend/src/app/components/source_editor/output_form.rs index 3cb17526f..65c8a022f 100644 --- a/frontend/src/app/components/source_editor/output_form.rs +++ b/frontend/src/app/components/source_editor/output_form.rs @@ -10,6 +10,8 @@ use yew::{component, html, use_context, Html, Properties}; pub struct ConfigOutputViewProps { pub(crate) block_id: BlockId, pub(crate) output: Option>, + #[prop_or(true)] + pub(crate) allow_write: bool, } #[component] @@ -30,7 +32,7 @@ pub fn ConfigOutputView(props: &ConfigOutputViewProps) -> Html { } }); - html! { } + html! { } } BlockType::OutputXtream => { let output = props.output.as_ref().and_then(|to| { @@ -41,7 +43,7 @@ pub fn ConfigOutputView(props: &ConfigOutputViewProps) -> Html { } }); - html! { } + html! { } } BlockType::OutputHdHomeRun => { let output = props.output.as_ref().and_then(|to| { @@ -52,7 +54,7 @@ pub fn ConfigOutputView(props: &ConfigOutputViewProps) -> Html { } }); - html! { } + html! { } } BlockType::OutputStrm => { let output = props.output.as_ref().and_then(|to| { @@ -63,7 +65,7 @@ pub fn ConfigOutputView(props: &ConfigOutputViewProps) -> Html { } }); - html! { } + html! { } } }, EditMode::Inactive => html! {}, diff --git a/frontend/src/app/components/source_editor/output_hdhomerun_form.rs b/frontend/src/app/components/source_editor/output_hdhomerun_form.rs index d8cee436e..58d5bcc02 100644 --- a/frontend/src/app/components/source_editor/output_hdhomerun_form.rs +++ b/frontend/src/app/components/source_editor/output_hdhomerun_form.rs @@ -3,7 +3,7 @@ use crate::{ config::HasFormData, select::Select, BlockId, BlockInstance, Card, DropDownOption, DropDownSelection, EditMode, SourceEditorContext, TextButton, }, - config_field_child, edit_field_text, generate_form_reducer, + config_field, config_field_child, config_field_custom, edit_field_text, generate_form_reducer, i18n::use_translation, }; use shared::model::{HdHomeRunTargetOutputDto, TargetOutputDto, TargetType}; @@ -30,6 +30,8 @@ generate_form_reducer!( pub struct HdHomeRunTargetOutputViewProps { pub(crate) block_id: BlockId, pub(crate) output: Option>, + #[prop_or(true)] + pub(crate) allow_write: bool, } #[component] @@ -64,36 +66,52 @@ pub fn HdHomeRunTargetOutputView(props: &HdHomeRunTargetOutputViewProps) -> Html } let render_output = || { - let output_form_state_1 = output_form_state.clone(); - html! { - - { edit_field_text!(output_form_state, translate.t(LABEL_DEVICE), device, HdHomeRunTargetOutputFormAction::Device) } - { edit_field_text!(output_form_state, translate.t(LABEL_USERNAME), username, HdHomeRunTargetOutputFormAction::Username) } - { config_field_child!(translate.t(LABEL_USE_OUTPUT), "OUTPUT_HDHOMERUN_FORM.USE_OUTPUT", { - html! { - { + output_form_state_1.dispatch(HdHomeRunTargetOutputFormAction::UseOutput(Some(TargetType::M3u))); + } + DropDownSelection::Single(option) => { + output_form_state_1.dispatch(HdHomeRunTargetOutputFormAction::UseOutput(Some(option.parse::().unwrap_or(TargetType::M3u)))); + } + DropDownSelection::Multi(options) => { + if let Some(first) = options.first() { + output_form_state_1.dispatch(HdHomeRunTargetOutputFormAction::UseOutput(Some(first.parse::().unwrap_or(TargetType::M3u)))); + } } } - } - })} - options={target_types.clone()} - /> - } - })} - + })} + options={target_types.clone()} + /> + } + })} + + } } }; @@ -124,10 +142,12 @@ pub fn HdHomeRunTargetOutputView(props: &HdHomeRunTargetOutputViewProps) -> Html icon="Cancel" title={ translate.t("LABEL.CANCEL")} onclick={handle_cancel}> - + if props.allow_write { + + }
{ render_output() } diff --git a/frontend/src/app/components/source_editor/output_m3u_form.rs b/frontend/src/app/components/source_editor/output_m3u_form.rs index 505b38e39..61a9ae18c 100644 --- a/frontend/src/app/components/source_editor/output_m3u_form.rs +++ b/frontend/src/app/components/source_editor/output_m3u_form.rs @@ -2,7 +2,8 @@ use crate::{ app::components::{ config::HasFormData, BlockId, BlockInstance, Card, EditMode, FilterInput, SourceEditorContext, TextButton, }, - config_field_child, edit_field_bool, edit_field_text_option, generate_form_reducer, + config_field_bool, config_field_child, config_field_custom, config_field_optional, edit_field_bool, + edit_field_text_option, generate_form_reducer, i18n::use_translation, }; use shared::model::{M3uTargetOutputDto, TargetOutputDto}; @@ -29,6 +30,8 @@ generate_form_reducer!( pub struct M3uTargetOutputViewProps { pub(crate) block_id: BlockId, pub(crate) output: Option>, + #[prop_or(true)] + pub(crate) allow_write: bool, } #[component] @@ -55,19 +58,33 @@ pub fn M3uTargetOutputView(props: &M3uTargetOutputViewProps) -> Html { let render_output = || { let output_form_state_1 = output_form_state.clone(); - html! { - - { edit_field_text_option!(output_form_state, translate.t(LABEL_FILENAME), filename, M3uTargetOutputFormAction::Filename) } - { edit_field_bool!(output_form_state, translate.t(LABEL_INCLUDE_TYPE_IN_URL), include_type_in_url, M3uTargetOutputFormAction::IncludeTypeInUrl) } - { edit_field_bool!(output_form_state, translate.t(LABEL_MASK_REDIRECT_URL), mask_redirect_url, M3uTargetOutputFormAction::MaskRedirectUrl) } - { config_field_child!(translate.t(LABEL_FILTER), "OUTPUT_M3U_FORM.FILTER", { - html! { - - } - })} - + if !props.allow_write { + html! { + + { config_field_optional!(output_form_state.form, translate.t(LABEL_FILENAME), filename) } + { config_field_bool!(output_form_state.form, translate.t(LABEL_INCLUDE_TYPE_IN_URL), include_type_in_url) } + { config_field_bool!(output_form_state.form, translate.t(LABEL_MASK_REDIRECT_URL), mask_redirect_url) } + { config_field_custom!( + translate.t(LABEL_FILTER), + output_form_state.form.filter.clone().unwrap_or_default() + ) } + + } + } else { + html! { + + { edit_field_text_option!(output_form_state, translate.t(LABEL_FILENAME), filename, M3uTargetOutputFormAction::Filename) } + { edit_field_bool!(output_form_state, translate.t(LABEL_INCLUDE_TYPE_IN_URL), include_type_in_url, M3uTargetOutputFormAction::IncludeTypeInUrl) } + { edit_field_bool!(output_form_state, translate.t(LABEL_MASK_REDIRECT_URL), mask_redirect_url, M3uTargetOutputFormAction::MaskRedirectUrl) } + { config_field_child!(translate.t(LABEL_FILTER), "OUTPUT_M3U_FORM.FILTER", { + html! { + + } + })} + + } } }; @@ -98,10 +115,12 @@ pub fn M3uTargetOutputView(props: &M3uTargetOutputViewProps) -> Html { icon="Cancel" title={ translate.t("LABEL.CANCEL")} onclick={handle_cancel}> - + if props.allow_write { + + }
{ render_output() } diff --git a/frontend/src/app/components/source_editor/output_strm_form.rs b/frontend/src/app/components/source_editor/output_strm_form.rs index 10021ed40..f97244c68 100644 --- a/frontend/src/app/components/source_editor/output_strm_form.rs +++ b/frontend/src/app/components/source_editor/output_strm_form.rs @@ -3,8 +3,8 @@ use crate::{ config::HasFormData, select::Select, BlockId, BlockInstance, Card, DropDownOption, DropDownSelection, EditMode, FilterInput, IconButton, Panel, SourceEditorContext, TextButton, }, - config_field_child, edit_field_bool, edit_field_list_option, edit_field_text, edit_field_text_option, - generate_form_reducer, + config_field, config_field_bool, config_field_child, config_field_custom, config_field_optional, edit_field_bool, + edit_field_list_option, edit_field_text, edit_field_text_option, generate_form_reducer, i18n::use_translation, }; use shared::{ @@ -87,6 +87,8 @@ generate_form_reducer!( pub struct StrmTargetOutputViewProps { pub(crate) block_id: BlockId, pub(crate) output: Option>, + #[prop_or(true)] + pub(crate) allow_write: bool, } #[component] @@ -137,54 +139,83 @@ pub fn StrmTargetOutputView(props: &StrmTargetOutputViewProps) -> Html { let render_main = || { let output_form_state_1 = output_form_state.clone(); let output_form_state_2 = output_form_state.clone(); - html! { - - { edit_field_text!(output_form_state, translate.t(LABEL_DIRECTORY), directory, StrmTargetOutputFormAction::Directory) } - { edit_field_text_option!(output_form_state, translate.t(LABEL_USERNAME), username, StrmTargetOutputFormAction::Username) } - { config_field_child!(translate.t(LABEL_EXPORT_STYLE), "OUTPUT_STRM_FORM.EXPORT_STYLE", { - html! { - { + output_form_state_1.dispatch(StrmTargetOutputFormAction::Style(StrmExportStyle::Kodi)); + } + DropDownSelection::Single(option) => { + output_form_state_1.dispatch(StrmTargetOutputFormAction::Style(option.parse::().unwrap_or(StrmExportStyle::Kodi))); + } + DropDownSelection::Multi(options) => { + if let Some(first) = options.first() { + output_form_state_1.dispatch(StrmTargetOutputFormAction::Style(first.parse::().unwrap_or(StrmExportStyle::Kodi))); + } } } - } - })} - options={export_styles.clone()} - /> - } - })} - { config_field_child!(translate.t(LABEL_FILTER), "OUTPUT_STRM_FORM.FILTER", { - html! { - - } - })} - + })} + options={export_styles.clone()} + /> + } + })} + { config_field_child!(translate.t(LABEL_FILTER), "OUTPUT_STRM_FORM.FILTER", { + html! { + + } + })} + + } } }; let render_options = || { - html! { - - { edit_field_bool!(output_form_state, translate.t(LABEL_FLAT), flat, StrmTargetOutputFormAction::Flat) } - { edit_field_bool!(output_form_state, translate.t(LABEL_UNDERSCORE_WHITESPACE), underscore_whitespace, StrmTargetOutputFormAction::UnderscoreWhitespace) } - { edit_field_bool!(output_form_state, translate.t(LABEL_CLEANUP), cleanup, StrmTargetOutputFormAction::Cleanup) } - { edit_field_bool!(output_form_state, translate.t(LABEL_ADD_QUALITY_TO_FILENAME), add_quality_to_filename, StrmTargetOutputFormAction::AddQualityToFilename) } - { edit_field_list_option!(output_form_state, translate.t(LABEL_STRM_PROPS), strm_props, StrmTargetOutputFormAction::StrmProps, translate.t(LABEL_ADD_PROPERTY)) } - + if !props.allow_write { + html! { + + { config_field_bool!(output_form_state.form, translate.t(LABEL_FLAT), flat) } + { config_field_bool!(output_form_state.form, translate.t(LABEL_UNDERSCORE_WHITESPACE), underscore_whitespace) } + { config_field_bool!(output_form_state.form, translate.t(LABEL_CLEANUP), cleanup) } + { config_field_bool!(output_form_state.form, translate.t(LABEL_ADD_QUALITY_TO_FILENAME), add_quality_to_filename) } + { config_field_custom!( + translate.t(LABEL_STRM_PROPS), + output_form_state.form.strm_props.as_ref().map_or_else(String::new, |props| props.join(", ")) + ) } + + } + } else { + html! { + + { edit_field_bool!(output_form_state, translate.t(LABEL_FLAT), flat, StrmTargetOutputFormAction::Flat) } + { edit_field_bool!(output_form_state, translate.t(LABEL_UNDERSCORE_WHITESPACE), underscore_whitespace, StrmTargetOutputFormAction::UnderscoreWhitespace) } + { edit_field_bool!(output_form_state, translate.t(LABEL_CLEANUP), cleanup, StrmTargetOutputFormAction::Cleanup) } + { edit_field_bool!(output_form_state, translate.t(LABEL_ADD_QUALITY_TO_FILENAME), add_quality_to_filename, StrmTargetOutputFormAction::AddQualityToFilename) } + { edit_field_list_option!(output_form_state, translate.t(LABEL_STRM_PROPS), strm_props, StrmTargetOutputFormAction::StrmProps, translate.t(LABEL_ADD_PROPERTY)) } + + } } }; @@ -239,10 +270,12 @@ pub fn StrmTargetOutputView(props: &StrmTargetOutputViewProps) -> Html { icon="Cancel" title={ translate.t("LABEL.CANCEL")} onclick={handle_cancel}> - + if props.allow_write { + + }
{ render_sidebar() } diff --git a/frontend/src/app/components/source_editor/output_xtream_form.rs b/frontend/src/app/components/source_editor/output_xtream_form.rs index 448c2878c..fc2e7458e 100644 --- a/frontend/src/app/components/source_editor/output_xtream_form.rs +++ b/frontend/src/app/components/source_editor/output_xtream_form.rs @@ -3,7 +3,8 @@ use crate::{ config::HasFormData, BlockId, BlockInstance, Card, EditMode, FilterInput, IconButton, Panel, SourceEditorContext, TextButton, TitledCard, TraktListItemForm, }, - config_field_child, edit_field_bool, edit_field_text, generate_form_reducer, + config_field, config_field_bool, config_field_child, config_field_custom, edit_field_bool, edit_field_text, + generate_form_reducer, i18n::use_translation, }; use shared::{ @@ -106,6 +107,8 @@ generate_form_reducer!( pub struct XtreamTargetOutputViewProps { pub(crate) block_id: BlockId, pub(crate) output: Option>, + #[prop_or(true)] + pub(crate) allow_write: bool, } #[component] @@ -212,23 +215,41 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html { let render_output = || { let output_form_state_1 = output_form_state.clone(); - html! { - - -
- { edit_field_bool!(output_form_state, translate.t(LABEL_LIVE), skip_live_direct_source, XtreamTargetOutputFormAction::SkipLiveDirectSource) } - { edit_field_bool!(output_form_state, translate.t(LABEL_VOD), skip_video_direct_source, XtreamTargetOutputFormAction::SkipVideoDirectSource) } - { edit_field_bool!(output_form_state, translate.t(LABEL_SERIES), skip_series_direct_source, XtreamTargetOutputFormAction::SkipSeriesDirectSource) } -
-
- { config_field_child!(translate.t(LABEL_FILTER), "OUTPUT_XTREAM_FORM.FILTER", { - html! { - - } - })} -
+ if !props.allow_write { + html! { + + +
+ { config_field_bool!(output_form_state.form, translate.t(LABEL_LIVE), skip_live_direct_source) } + { config_field_bool!(output_form_state.form, translate.t(LABEL_VOD), skip_video_direct_source) } + { config_field_bool!(output_form_state.form, translate.t(LABEL_SERIES), skip_series_direct_source) } +
+
+ { config_field_custom!( + translate.t(LABEL_FILTER), + output_form_state.form.filter.clone().unwrap_or_default() + ) } +
+ } + } else { + html! { + + +
+ { edit_field_bool!(output_form_state, translate.t(LABEL_LIVE), skip_live_direct_source, XtreamTargetOutputFormAction::SkipLiveDirectSource) } + { edit_field_bool!(output_form_state, translate.t(LABEL_VOD), skip_video_direct_source, XtreamTargetOutputFormAction::SkipVideoDirectSource) } + { edit_field_bool!(output_form_state, translate.t(LABEL_SERIES), skip_series_direct_source, XtreamTargetOutputFormAction::SkipSeriesDirectSource) } +
+
+ { config_field_child!(translate.t(LABEL_FILTER), "OUTPUT_XTREAM_FORM.FILTER", { + html! { + + } + })} +
+ } } }; @@ -244,16 +265,31 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html { } else { - // Trakt API Configuration - { edit_field_bool!(trakt_form, translate.t(LABEL_ENABLED), enabled, TraktConfigFormAction::Enabled) } + { if props.allow_write { + html! { { edit_field_bool!(trakt_form, translate.t(LABEL_ENABLED), enabled, TraktConfigFormAction::Enabled) } } + } else { + html! { { config_field_bool!(trakt_form.form, translate.t(LABEL_ENABLED), enabled) } } + }}

{translate.t(LABEL_API_CONFIGURATION)}

- { edit_field_text!(trakt_api_form, translate.t(LABEL_TRAKT_API_KEY), api_key, TraktApiConfigFormAction::ApiKey) } - { edit_field_text!(trakt_api_form, translate.t(LABEL_TRAKT_API_VERSION), version, TraktApiConfigFormAction::Version) } - { edit_field_text!(trakt_api_form, translate.t(LABEL_TRAKT_API_URL), url, TraktApiConfigFormAction::Url) } - { edit_field_text!(trakt_api_form, translate.t(LABEL_USER_AGENT), user_agent, TraktApiConfigFormAction::UserAgent) } + if props.allow_write { + <> + { edit_field_text!(trakt_api_form, translate.t(LABEL_TRAKT_API_KEY), api_key, TraktApiConfigFormAction::ApiKey) } + { edit_field_text!(trakt_api_form, translate.t(LABEL_TRAKT_API_VERSION), version, TraktApiConfigFormAction::Version) } + { edit_field_text!(trakt_api_form, translate.t(LABEL_TRAKT_API_URL), url, TraktApiConfigFormAction::Url) } + { edit_field_text!(trakt_api_form, translate.t(LABEL_USER_AGENT), user_agent, TraktApiConfigFormAction::UserAgent) } + + } else { + <> + { config_field!(trakt_api_form.form, translate.t(LABEL_TRAKT_API_KEY), api_key) } + { config_field!(trakt_api_form.form, translate.t(LABEL_TRAKT_API_VERSION), version) } + { config_field!(trakt_api_form.form, translate.t(LABEL_TRAKT_API_URL), url) } + { config_field!(trakt_api_form.form, translate.t(LABEL_USER_AGENT), user_agent) } + + }
// Trakt Lists @@ -264,10 +300,12 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
for item in (*trakt_lists_list).iter().enumerate() {
- + if props.allow_write { + + }
{&item.1.user} @@ -290,13 +328,15 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html { }
- + if props.allow_write { + + }
} })} @@ -383,10 +423,12 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html { icon="Cancel" title={ translate.t("LABEL.CANCEL")} onclick={handle_cancel}> - + if props.allow_write { + + }
{ render_sidebar() } diff --git a/frontend/src/app/components/source_editor/provider_item_form.rs b/frontend/src/app/components/source_editor/provider_item_form.rs index 607214118..43ce98af9 100644 --- a/frontend/src/app/components/source_editor/provider_item_form.rs +++ b/frontend/src/app/components/source_editor/provider_item_form.rs @@ -1,6 +1,7 @@ use crate::{ app::components::{select::Select, Card, DropDownOption, DropDownSelection, Tag, TagList, TextButton}, - config_field_child, edit_field_bool, edit_field_number_u64, edit_field_text, generate_form_reducer, + config_field, config_field_bool, config_field_child, config_field_custom, edit_field_bool, edit_field_number_u64, + edit_field_text, generate_form_reducer, i18n::use_translation, }; use shared::{ @@ -151,6 +152,8 @@ pub struct ProviderItemFormProps { pub on_cancel: Callback<()>, #[prop_or_default] pub initial: Option, + #[prop_or(false)] + pub readonly: bool, } #[component] @@ -336,12 +339,16 @@ pub fn ProviderItemForm(props: &ProviderItemFormProps) -> Html { html! { - { edit_field_text!(form_state, translate.t(LABEL_PROVIDER_NAME), name, ProviderFormAction::Name) } + if props.readonly { + { config_field!(form_state.form, translate.t(LABEL_PROVIDER_NAME), name) } + } else { + { edit_field_text!(form_state, translate.t(LABEL_PROVIDER_NAME), name, ProviderFormAction::Name) } + } { config_field_child!(translate.t(LABEL_PROVIDER_URLS), "PROVIDER_FORM.URLS", { html! { @@ -354,65 +361,94 @@ pub fn ProviderItemForm(props: &ProviderItemFormProps) -> Html { }

{ translate.t(LABEL_PROVIDER_DNS) }

- { edit_field_bool!(dns_state, translate.t(LABEL_DNS_ENABLED), enabled, ProviderDnsFormAction::Enabled) } -
- { edit_field_number_u64!(dns_state, translate.t(LABEL_DNS_REFRESH_SECS), refresh_secs, ProviderDnsFormAction::RefreshSecs) } -
- | { - let parsed = value.and_then(|v| usize::try_from(v).ok()); - dns_state.dispatch(ProviderDnsFormAction::MaxAddrs(parsed)); - } - })} - /> + if props.readonly { + { config_field_bool!(dns_state.form, translate.t(LABEL_DNS_ENABLED), enabled) } +
+ { config_field_custom!(translate.t(LABEL_DNS_REFRESH_SECS), dns_state.form.refresh_secs.to_string()) } + { config_field_custom!( + translate.t(LABEL_DNS_MAX_ADDRS), + dns_state.form.max_addrs.map_or_else(String::new, |value| value.to_string()) + ) }
-
- { config_field_child!(translate.t(LABEL_DNS_PREFER), "PROVIDER_FORM.DNS.PREFER", { - html! { - - } - })} - { edit_field_bool!(dns_state, translate.t(LABEL_DNS_KEEP_VHOST), keep_vhost, ProviderDnsFormAction::KeepVhost) } - { config_field_child!(translate.t(LABEL_DNS_ON_RESOLVE_ERROR), "PROVIDER_FORM.DNS.ON_RESOLVE_ERROR", { - html! { - - } - })} + { config_field_custom!(translate.t(LABEL_DNS_PREFER), dns_prefer_to_id(dns_state.form.prefer).to_string()) } + { config_field_custom!( + translate.t(LABEL_DNS_SCHEMES), + dns_state.form.schemes.as_ref().map_or_else(String::new, |schemes| { + schemes.iter().map(|scheme| scheme_to_id(*scheme)).collect::>().join(", ") + }) + ) } + { config_field_bool!(dns_state.form, translate.t(LABEL_DNS_KEEP_VHOST), keep_vhost) } + { config_field_custom!( + translate.t(LABEL_DNS_ON_RESOLVE_ERROR), + on_resolve_error_to_id(dns_state.form.on_resolve_error).to_string() + ) } + { config_field_custom!( + translate.t(LABEL_DNS_ON_CONNECT_ERROR), + on_connect_error_to_id(dns_state.form.on_connect_error).to_string() + ) } + } else { + <> + { edit_field_bool!(dns_state, translate.t(LABEL_DNS_ENABLED), enabled, ProviderDnsFormAction::Enabled) } +
+ { edit_field_number_u64!(dns_state, translate.t(LABEL_DNS_REFRESH_SECS), refresh_secs, ProviderDnsFormAction::RefreshSecs) } +
+ | { + let parsed = value.and_then(|v| usize::try_from(v).ok()); + dns_state.dispatch(ProviderDnsFormAction::MaxAddrs(parsed)); + } + })} + /> +
+
+ { config_field_child!(translate.t(LABEL_DNS_PREFER), "PROVIDER_FORM.DNS.PREFER", { + html! { + + } + })} + { edit_field_bool!(dns_state, translate.t(LABEL_DNS_KEEP_VHOST), keep_vhost, ProviderDnsFormAction::KeepVhost) } + { config_field_child!(translate.t(LABEL_DNS_ON_RESOLVE_ERROR), "PROVIDER_FORM.DNS.ON_RESOLVE_ERROR", { + html! { + + } + })} + + }
Html { title={translate.t("LABEL.CANCEL")} onclick={handle_cancel} /> - + if !props.readonly { + + }
} diff --git a/frontend/src/app/components/source_editor/target_form.rs b/frontend/src/app/components/source_editor/target_form.rs index f8696c231..4e666fd75 100644 --- a/frontend/src/app/components/source_editor/target_form.rs +++ b/frontend/src/app/components/source_editor/target_form.rs @@ -3,7 +3,8 @@ use crate::{ config::HasFormData, select::Select, BlockId, BlockInstance, Card, ClusterFlagsInput, ClusterFlagsInputMode, DropDownOption, DropDownSelection, EditMode, FilterInput, IconButton, Panel, SourceEditorContext, TextButton, }, - config_field_child, edit_field_bool, edit_field_list_option, edit_field_text, generate_form_reducer, + config_field, config_field_bool, config_field_child, config_field_custom, edit_field_bool, edit_field_list_option, + edit_field_text, generate_form_reducer, i18n::use_translation, }; use shared::{ @@ -98,6 +99,8 @@ generate_form_reducer!( pub struct ConfigTargetViewProps { pub(crate) block_id: BlockId, pub(crate) target: Option>, + #[prop_or(true)] + pub(crate) allow_write: bool, } #[component] @@ -158,72 +161,113 @@ pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html { let render_options = || { let target_options_state_1 = target_options_state.clone(); - html! { - -
- { edit_field_bool!(target_options_state, translate.t(LABEL_IGNORE_LOGO), ignore_logo, ConfigTargetOptionsFormAction::IgnoreLogo) } - { edit_field_bool!(target_options_state, translate.t(LABEL_SHARE_LIVE_STREAMS), share_live_streams, ConfigTargetOptionsFormAction::ShareLiveStreams) } -
- { edit_field_bool!(target_options_state, translate.t(LABEL_REMOVE_DUPLICATES), remove_duplicates, ConfigTargetOptionsFormAction::RemoveDuplicates) } - { config_field_child!(translate.t(LABEL_FORCE_REDIRECT), "TARGET_FORM.FORCE_REDIRECT", { - html! { - )| { - target_options_state_1.dispatch(ConfigTargetOptionsFormAction::ForceRedirect(flags)); - })} - /> - }})} -
+ if !props.allow_write { + html! { + +
+ { config_field_bool!(target_options_state.form, translate.t(LABEL_IGNORE_LOGO), ignore_logo) } + { config_field_bool!(target_options_state.form, translate.t(LABEL_SHARE_LIVE_STREAMS), share_live_streams) } +
+ { config_field_bool!(target_options_state.form, translate.t(LABEL_REMOVE_DUPLICATES), remove_duplicates) } + { config_field_custom!( + translate.t(LABEL_FORCE_REDIRECT), + target_options_state.form.force_redirect.map_or_else(String::new, |flags| flags.to_string()) + ) } +
+ } + } else { + html! { + +
+ { edit_field_bool!(target_options_state, translate.t(LABEL_IGNORE_LOGO), ignore_logo, ConfigTargetOptionsFormAction::IgnoreLogo) } + { edit_field_bool!(target_options_state, translate.t(LABEL_SHARE_LIVE_STREAMS), share_live_streams, ConfigTargetOptionsFormAction::ShareLiveStreams) } +
+ { edit_field_bool!(target_options_state, translate.t(LABEL_REMOVE_DUPLICATES), remove_duplicates, ConfigTargetOptionsFormAction::RemoveDuplicates) } + { config_field_child!(translate.t(LABEL_FORCE_REDIRECT), "TARGET_FORM.FORCE_REDIRECT", { + html! { + )| { + target_options_state_1.dispatch(ConfigTargetOptionsFormAction::ForceRedirect(flags)); + })} + /> + }})} +
+ } } }; let render_target = || { let target_form_state_1 = target_form_state.clone(); let target_form_state_2 = target_form_state.clone(); - html! { - -
- { edit_field_bool!(target_form_state, translate.t(LABEL_ENABLED), enabled, ConfigTargetFormAction::Enabled) } - { edit_field_bool!(target_form_state, translate.t(LABEL_USE_MEMORY_CACHE), use_memory_cache, ConfigTargetFormAction::UseMemoryCache) } -
- { edit_field_text!(target_form_state, translate.t(LABEL_NAME), name, ConfigTargetFormAction::Name) } - { config_field_child!(translate.t(LABEL_FILTER), "TARGET_FORM.FILTER", { - html! { - | { - target_form_state_2.dispatch(ConfigTargetFormAction::Filter(new_filter.unwrap_or_default())); - })} /> - } - })} + if !props.allow_write { + html! { + +
+ { config_field_bool!(target_form_state.form, translate.t(LABEL_ENABLED), enabled) } + { config_field_bool!(target_form_state.form, translate.t(LABEL_USE_MEMORY_CACHE), use_memory_cache) } +
+ { config_field!(target_form_state.form, translate.t(LABEL_NAME), name) } + { config_field_custom!(translate.t(LABEL_FILTER), target_form_state.form.filter.clone()) } + { config_field_custom!( + translate.t(LABEL_PROCESSING_ORDER), + target_form_state.form.processing_order.to_string() + ) } + { config_field_custom!( + translate.t(LABEL_MAPPING), + target_form_state.form.mapping.as_ref().map_or_else(String::new, |values| values.join(", ")) + ) } + { config_field_custom!( + translate.t(LABEL_WATCH), + target_form_state.form.watch.as_ref().map_or_else(String::new, |values| values.join(", ")) + ) } +
+ } + } else { + html! { + +
+ { edit_field_bool!(target_form_state, translate.t(LABEL_ENABLED), enabled, ConfigTargetFormAction::Enabled) } + { edit_field_bool!(target_form_state, translate.t(LABEL_USE_MEMORY_CACHE), use_memory_cache, ConfigTargetFormAction::UseMemoryCache) } +
+ { edit_field_text!(target_form_state, translate.t(LABEL_NAME), name, ConfigTargetFormAction::Name) } + { config_field_child!(translate.t(LABEL_FILTER), "TARGET_FORM.FILTER", { + html! { + | { + target_form_state_2.dispatch(ConfigTargetFormAction::Filter(new_filter.unwrap_or_default())); + })} /> + } + })} - { config_field_child!(translate.t(LABEL_PROCESSING_ORDER), "TARGET_FORM.PROCESSING_ORDER", { - html! { - { + target_form_state_1.dispatch(ConfigTargetFormAction::ProcessingOrder(ProcessingOrder::Frm)); + } + DropDownSelection::Single(option) => { + target_form_state_1.dispatch(ConfigTargetFormAction::ProcessingOrder(option.parse::().unwrap_or(ProcessingOrder::Frm))); + } + DropDownSelection::Multi(options) => { + if let Some(first) = options.first() { + target_form_state_1.dispatch(ConfigTargetFormAction::ProcessingOrder(first.parse::().unwrap_or(ProcessingOrder::Frm))); + } + } } - } - } - })} - options={processing_orders.clone()} - /> - }})} - { edit_field_list_option!(target_form_state, translate.t(LABEL_MAPPING), mapping, ConfigTargetFormAction::Mapping, translate.t(LABEL_ADD_MAPPING)) } - { edit_field_list_option!(target_form_state, translate.t(LABEL_WATCH), watch, ConfigTargetFormAction::Watch, translate.t(LABEL_ADD_WATCH)) } -
+ })} + options={processing_orders.clone()} + /> + }})} + { edit_field_list_option!(target_form_state, translate.t(LABEL_MAPPING), mapping, ConfigTargetFormAction::Mapping, translate.t(LABEL_ADD_MAPPING)) } + { edit_field_list_option!(target_form_state, translate.t(LABEL_WATCH), watch, ConfigTargetFormAction::Watch, translate.t(LABEL_ADD_WATCH)) } +
+ } } }; @@ -282,10 +326,12 @@ pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html { icon="Cancel" title={ translate.t("LABEL.CANCEL")} onclick={handle_cancel}> - + if props.allow_write { + + }
{ render_sidebar() } diff --git a/frontend/src/app/components/source_editor/trakt_list_item_form.rs b/frontend/src/app/components/source_editor/trakt_list_item_form.rs index ae7b8ad00..1414ecfa6 100644 --- a/frontend/src/app/components/source_editor/trakt_list_item_form.rs +++ b/frontend/src/app/components/source_editor/trakt_list_item_form.rs @@ -1,6 +1,7 @@ use crate::{ app::components::{select::Select, Card, DropDownOption, DropDownSelection, TextButton}, - config_field_child, edit_field_number_u8, edit_field_text, generate_form_reducer, + config_field, config_field_child, config_field_custom, edit_field_number_u8, edit_field_text, + generate_form_reducer, i18n::use_translation, }; use shared::model::{TraktContentType, TraktListConfigDto}; @@ -30,6 +31,8 @@ pub struct TraktListItemFormProps { pub on_cancel: Callback<()>, #[prop_or_default] pub initial: Option, + #[prop_or(false)] + pub readonly: bool, } #[component] @@ -91,29 +94,48 @@ pub fn TraktListItemForm(props: &TraktListItemFormProps) -> Html { html! { - { edit_field_text!(form_state, translate.t(LABEL_TRAKT_USER), user, TraktListFormAction::User) } - { edit_field_text!(form_state, translate.t(LABEL_TRAKT_LIST_SLUG), list_slug, TraktListFormAction::ListSlug) } - { edit_field_text!(form_state, translate.t(LABEL_TRAKT_CATEGORY_NAME), category_name, TraktListFormAction::CategoryName) } + if props.readonly { + { config_field!(form_state.form, translate.t(LABEL_TRAKT_USER), user) } + { config_field!(form_state.form, translate.t(LABEL_TRAKT_LIST_SLUG), list_slug) } + { config_field!(form_state.form, translate.t(LABEL_TRAKT_CATEGORY_NAME), category_name) } + } else { + <> + { edit_field_text!(form_state, translate.t(LABEL_TRAKT_USER), user, TraktListFormAction::User) } + { edit_field_text!(form_state, translate.t(LABEL_TRAKT_LIST_SLUG), list_slug, TraktListFormAction::ListSlug) } + { edit_field_text!(form_state, translate.t(LABEL_TRAKT_CATEGORY_NAME), category_name, TraktListFormAction::CategoryName) } + + } - { config_field_child!(translate.t(LABEL_TRAKT_CONTENT_TYPE), "TRAKT_LIST_FORM.TRAKT_CONTENT_TYPE", { - let form_state_ct = form_state.clone(); - html! { - () { + form_state_ct.dispatch(TraktListFormAction::ContentType(ct)); + } } - } - })} - options={content_type_options.clone()} - /> - } - })} + })} + options={content_type_options.clone()} + /> + } + })} + } - { edit_field_number_u8!(form_state, translate.t(LABEL_TRAKT_FUZZY_MATCH_THRESHOLD), fuzzy_match_threshold, TraktListFormAction::FuzzyMatchThreshold) } + if props.readonly { + { config_field_custom!( + translate.t(LABEL_TRAKT_FUZZY_MATCH_THRESHOLD), + form_state.form.fuzzy_match_threshold.to_string() + ) } + } else { + { edit_field_number_u8!(form_state, translate.t(LABEL_TRAKT_FUZZY_MATCH_THRESHOLD), fuzzy_match_threshold, TraktListFormAction::FuzzyMatchThreshold) } + }
Html { title={translate.t("LABEL.CANCEL")} onclick={handle_cancel} /> - + if !props.readonly { + + }
} diff --git a/shared/src/model/stream_properties.rs b/shared/src/model/stream_properties.rs index 6cf08841d..fb76cb52f 100644 --- a/shared/src/model/stream_properties.rs +++ b/shared/src/model/stream_properties.rs @@ -741,14 +741,11 @@ impl VideoStreamProperties { } impl SeriesStreamProperties { - pub fn from_info

(info: &XtreamSeriesInfo, pli: &P) -> SeriesStreamProperties - where - P: PlaylistEntry, - { + fn from_info_base(info: &XtreamSeriesInfo, series_id: u32) -> SeriesStreamProperties { SeriesStreamProperties { name: info.info.name.clone(), category_id: info.info.category_id, - series_id: pli.get_virtual_id(), + series_id, backdrop_path: info.info.backdrop_path.clone(), cast: info.info.cast.clone(), cover: info.info.cover.clone(), @@ -815,6 +812,17 @@ impl SeriesStreamProperties { } } + pub fn from_info

(info: &XtreamSeriesInfo, pli: &P) -> SeriesStreamProperties + where + P: PlaylistEntry, + { + Self::from_info_base(info, pli.get_virtual_id()) + } + + pub fn from_info_without_existing(info: &XtreamSeriesInfo, series_id: u32) -> SeriesStreamProperties { + Self::from_info_base(info, series_id) + } + pub fn from_info_doc(info: &XtreamSeriesInfoDoc, series_id: u32) -> SeriesStreamProperties { let tmdb = info.info.tmdb.parse::().ok(); SeriesStreamProperties {