diff --git a/backend/src/api/endpoints/v1_api.rs b/backend/src/api/endpoints/v1_api.rs index 4a3b6c8dc..ac50215bd 100644 --- a/backend/src/api/endpoints/v1_api.rs +++ b/backend/src/api/endpoints/v1_api.rs @@ -440,14 +440,12 @@ pub fn v1_api_register(web_auth_enabled: bool, app_state: Arc, web_ui_ .route("/playlist/update", axum::routing::post(playlist_update)) .route("/playlist", axum::routing::post(playlist_content)) .route("/file/download", axum::routing::post(download_api::queue_download_file)) - .route("/file/download/info", axum::routing::get(download_api::download_file_info)); - let config = app_state.app_config.config.load(); - if config.ipcheck.is_some() { - router = router.route("/ipinfo", axum::routing::get(ipinfo)); - } + .route("/file/download/info", axum::routing::get(download_api::download_file_info)) + .route("/ipinfo", axum::routing::get(ipinfo)); if web_auth_enabled { router = router.route_layer(axum::middleware::from_fn_with_state(Arc::clone(&app_state), validator_admin)); } + let config = app_state.app_config.config.load(); let mut base_router = axum::Router::new(); if config.web_ui.as_ref().is_none_or(|c| c.user_ui_enabled) { diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index 90b218e8b..0dd01a66e 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -74,7 +74,8 @@ "CACHE": "Cache", "STREAM": "Stream", "LOG": "Log", - "LOG_LEVEL": "Log level", + "LOG_LEVEL": "Log Level", + "LOG_ACTIVE_USER": "Log Active User", "CONFIG": "Config", "TELEGRAM": "Telegram", "REST": "Rest", @@ -277,7 +278,8 @@ "ADD_CHAT_ID": "Add ChatId", "ADD_ATTRIBUTE": "Add Attribute", "ADD_EXTENSION": "Add Extension", - "ADD_DEVICE": "Add Device" + "ADD_DEVICE": "Add Device", + "EXTENDED_ATTRIBUTES": "Extended Attributes" }, "TITLE": { "USER_BOUQUET_EDITOR": "User group editor" diff --git a/frontend/public/assets/icons.json b/frontend/public/assets/icons.json index 1ed9c1790..b9220bea6 100644 --- a/frontend/public/assets/icons.json +++ b/frontend/public/assets/icons.json @@ -479,5 +479,9 @@ { "keys": ["Unlocked"], "path": "M 16.240516,2 C 13.061569,2 10.481561,4.1356468 10.481561,6.7669545 V 8.6530441 H 5.071266 4.0475106 C 2.9213176,8.6530441 2,9.5110844 2,10.559827 v 9.533391 C 2,21.141922 2.9213176,22 4.0475106,22 H 16.333103 c 1.126232,0 2.048041,-0.858078 2.048041,-1.906782 v -9.533391 c 0,-1.0487426 -0.921809,-1.9067829 -2.048041,-1.9067829 H 15.309348 12.785143 V 6.7669545 c 0,-1.5826042 1.543435,-2.8601727 3.455373,-2.8601727 1.911984,0 3.455372,1.2775685 3.455372,2.8601727 v 1.9067818 h -0.02434 c 0.71388,0.050681 1.566852,0.023278 2.325274,0.023278 -0.0041,-3.003e-4 -0.01,-0.00329 0.0032,-0.023278 V 6.7669545 C 22.000022,4.1356468 19.420015,2 16.241067,2 Z M 4.0475106,10.559827 H 16.333103 v 9.533391 H 4.0475106 Z" + }, + { + "keys": ["Note", "Text", "Log"], + "path": "M19 5v9h-5v5H5V5zm0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h10l6-6V5c0-1.1-.9-2-2-2m-7 11H7v-2h5zm5-4H7V8h10z" } ] \ No newline at end of file diff --git a/frontend/scss/app/_component.scss b/frontend/scss/app/_component.scss index e941c8a46..d1dc29fcc 100644 --- a/frontend/scss/app/_component.scss +++ b/frontend/scss/app/_component.scss @@ -69,4 +69,5 @@ @forward "components/select"; @forward "components/form"; @forward "components/api_user/api_user_view"; -@forward "components/keyvalue_editor"; \ No newline at end of file +@forward "components/keyvalue_editor"; +@forward "components/loading_screen"; diff --git a/frontend/scss/app/components/_loading_screen.scss b/frontend/scss/app/components/_loading_screen.scss new file mode 100644 index 000000000..faa7e0a24 --- /dev/null +++ b/frontend/scss/app/components/_loading_screen.scss @@ -0,0 +1,23 @@ +.tp__loading-screen { + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + height: 100vh; + width: 100vw; + perspective: 20rem; + .svg-logo { + fill: var(--primary-color); + transform-style: preserve-3d; + animation: spinY 3s linear infinite; + } +} + +@keyframes spinY { + from { + transform: rotateY(0deg); + } + to { + transform: rotateY(360deg); + } +} \ No newline at end of file diff --git a/frontend/scss/app/components/config/_config_view.scss b/frontend/scss/app/components/config/_config_view.scss index 006df44a2..563143cab 100644 --- a/frontend/scss/app/components/config/_config_view.scss +++ b/frontend/scss/app/components/config/_config_view.scss @@ -165,4 +165,24 @@ border-color: var(--warn-color); } } +} + +.tp__hdhomerun__device-extended-fields { + .tp__collapse-panel { + &__header { + padding: var(--padding-micro) 0; + font-size: 1.1rem; + } + + &__body { + padding-top: var(--padding-default); + } + } +} + +.tp__messaging-config-view__notify-on { + display: flex; + flex-flow: row wrap; + gap: var(--gap-default); + box-sizing: border-box; } \ No newline at end of file diff --git a/frontend/src/app/components/config/config_page.rs b/frontend/src/app/components/config/config_page.rs index a33ab5fd6..4e6e7a9f5 100644 --- a/frontend/src/app/components/config/config_page.rs +++ b/frontend/src/app/components/config/config_page.rs @@ -1,12 +1,12 @@ use shared::error::TuliproxError; use shared::info_err; -use shared::model::{ConfigApiDto, HdHomeRunConfigDto, IpCheckConfigDto, MainConfigDto, MessagingConfigDto, - ProxyConfigDto, ReverseProxyConfigDto, SchedulesConfigDto, VideoConfigDto, WebUiConfigDto}; +use shared::model::{ConfigApiDto, HdHomeRunConfigDto, IpCheckConfigDto, LogConfigDto, MainConfigDto, MessagingConfigDto, ProxyConfigDto, ReverseProxyConfigDto, SchedulesConfigDto, VideoConfigDto, WebUiConfigDto}; use std::fmt; use std::str::FromStr; const MAIN_PAGE: &str = "main"; const API_PAGE: &str = "api"; +const LOG_PAGE: &str = "log"; const SCHEDULES_PAGE: &str = "schedules"; const MESSAGING_PAGE: &str = "messaging"; const WEBUI_PAGE: &str = "webui"; @@ -20,6 +20,7 @@ const VIDEO_PAGE: &str = "video"; pub enum ConfigPage { Main, Api, + Log, Schedules, Video, Messaging, @@ -37,6 +38,7 @@ impl FromStr for ConfigPage { match s.to_lowercase().as_str() { MAIN_PAGE => Ok(ConfigPage::Main), API_PAGE => Ok(ConfigPage::Api), + LOG_PAGE => Ok(ConfigPage::Log), SCHEDULES_PAGE => Ok(ConfigPage::Schedules), VIDEO_PAGE => Ok(ConfigPage::Video), MESSAGING_PAGE => Ok(ConfigPage::Messaging), @@ -55,6 +57,7 @@ impl fmt::Display for ConfigPage { let s = match self { ConfigPage::Main => MAIN_PAGE, ConfigPage::Api => API_PAGE, + ConfigPage::Log => LOG_PAGE, ConfigPage::Schedules => SCHEDULES_PAGE, ConfigPage::Video => VIDEO_PAGE, ConfigPage::Messaging => MESSAGING_PAGE, @@ -72,6 +75,7 @@ impl fmt::Display for ConfigPage { pub enum ConfigForm { Main(bool, MainConfigDto), Api(bool, ConfigApiDto), + Log(bool, LogConfigDto), Schedules(bool, SchedulesConfigDto), Video(bool, VideoConfigDto), Messaging(bool, MessagingConfigDto), @@ -86,6 +90,7 @@ impl ConfigForm { pub(crate) fn is_modified(&self) -> bool { matches!(self, ConfigForm::Main(true, _) | ConfigForm::Api(true, _) + | ConfigForm::Log(true, _) | ConfigForm::Schedules(true, _) | ConfigForm::Video(true, _) | ConfigForm::Messaging(true, _) diff --git a/frontend/src/app/components/config/config_update.rs b/frontend/src/app/components/config/config_update.rs new file mode 100644 index 000000000..23f63a5c3 --- /dev/null +++ b/frontend/src/app/components/config/config_update.rs @@ -0,0 +1,37 @@ +use shared::model::ConfigDto; +use crate::app::components::config::config_page::ConfigForm; + +macro_rules! set_config_field { + ($main_config:expr, $config:expr, $field:ident) => { + if $config.is_empty() { + $main_config.$field = None; + } else { + $config.clean(); + $main_config.$field = Some($config); + } + }; +} + +pub fn update_config(config: &mut ConfigDto, forms: Vec) { + for form in forms { + match form { + ConfigForm::Main(_, main_cfg) => config.update_from_main_config(&main_cfg), + ConfigForm::Api(_, api_cfg) => config.api = api_cfg, + ConfigForm::Log(_, mut log_cfg) => set_config_field!(config, log_cfg, log), + ConfigForm::Schedules(_, schedules_cfg) => { + if schedules_cfg.schedules.is_none() || schedules_cfg.schedules.as_ref().is_some_and(|s| s.is_empty()) { + config.schedules = None; + } else { + config.schedules = schedules_cfg.schedules.clone(); + } + }, + ConfigForm::Video(_, mut video_cfg) => set_config_field!(config, video_cfg, video), + ConfigForm::Messaging(_, mut messaging_cfg) => set_config_field!(config, messaging_cfg, messaging), + ConfigForm::WebUi(_, mut web_ui_cfg) => set_config_field!(config, web_ui_cfg, web_ui), + ConfigForm::ReverseProxy(_, mut reverse_proxy_cfg) => set_config_field!(config, reverse_proxy_cfg, reverse_proxy), + ConfigForm::HdHomerun(_, mut hdhr_cfg) => set_config_field!(config, hdhr_cfg, hdhomerun), + ConfigForm::Proxy(_, mut proxy_cfg) => set_config_field!(config, proxy_cfg, proxy), + ConfigForm::IpCheck(_, mut ipcheck_cfg) => set_config_field!(config, ipcheck_cfg, ipcheck), + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/config_view.rs b/frontend/src/app/components/config/config_view.rs index 10ea2c23a..db8647d81 100644 --- a/frontend/src/app/components/config/config_view.rs +++ b/frontend/src/app/components/config/config_view.rs @@ -1,19 +1,43 @@ use crate::app::components::config::config_page::{ConfigForm, ConfigPage}; use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::config::{ApiConfigView, HdHomerunConfigView, IpCheckConfigView, MainConfigView, MessagingConfigView, ProxyConfigView, ReverseProxyConfigView, SchedulesConfigView, VideoConfigView, WebUiConfigView}; +use crate::app::components::config::{ApiConfigView, HdHomerunConfigView, IpCheckConfigView, LogConfigView, MainConfigView, MessagingConfigView, ProxyConfigView, ReverseProxyConfigView, SchedulesConfigView, VideoConfigView, WebUiConfigView}; use crate::app::components::{Card, TabItem, TabSet, TextButton}; use crate::html_if; use std::str::FromStr; -use log::warn; +use yew::platform::spawn_local; use yew::prelude::*; use yew_i18n::use_translation; +use shared::model::ConfigDto; +use crate::app::components::config::config_update::update_config; +use crate::app::{ConfigContext}; +use crate::hooks::use_service_context; + +// ==== Label Konstanten ==== +const LABEL_MAIN: &str = "LABEL.MAIN"; +const LABEL_API: &str = "LABEL.API"; +const LABEL_LOG: &str = "LABEL.LOG"; +const LABEL_SCHEDULES: &str = "LABEL.SCHEDULES"; +const LABEL_MESSAGING: &str = "LABEL.MESSAGING"; +const LABEL_WEB_UI: &str = "LABEL.WEB_UI"; +const LABEL_REVERSE_PROXY: &str = "LABEL.REVERSE_PROXY"; +const LABEL_HDHOMERUN_CONFIG: &str = "LABEL.HDHOMERUN_CONFIG"; +const LABEL_PROXY: &str = "LABEL.PROXY"; +const LABEL_IP_CHECK: &str = "LABEL.IP_CHECK"; +const LABEL_VIDEO: &str = "LABEL.VIDEO"; +const LABEL_CONFIG: &str = "LABEL.CONFIG"; +const LABEL_EDIT: &str = "LABEL.EDIT"; +const LABEL_VIEW: &str = "LABEL.VIEW"; +const LABEL_SAVE: &str = "LABEL.SAVE"; +// ========================== macro_rules! collect_modified { ($forms:expr, [ $($field:ident),+ $(,)? ]) => {{ let mut modified = Vec::new(); $( - if $forms.$field.as_ref().is_some_and(|s| s.is_modified()) { - modified.push($forms.$field.clone()); + if let Some(form) = $forms.$field.as_ref() { + if form.is_modified() { + modified.push(form.clone()); + } } )+ modified @@ -24,6 +48,7 @@ fn config_form_to_config_page(form: &ConfigForm) -> ConfigPage { match form { ConfigForm::Main(_, _) => ConfigPage::Main, ConfigForm::Api(_, _) => ConfigPage::Api, + ConfigForm::Log(_, _) => ConfigPage::Log, ConfigForm::Schedules(_, _) => ConfigPage::Schedules, ConfigForm::Video(_, _) => ConfigPage::Video, ConfigForm::Messaging(_, _) => ConfigPage::Messaging, @@ -39,6 +64,7 @@ fn config_form_to_config_page(form: &ConfigForm) -> ConfigPage { struct ConfigFormState { pub main: Option, pub api: Option, + pub log: Option, pub schedules: Option, pub video: Option, pub messaging: Option, @@ -52,6 +78,9 @@ struct ConfigFormState { #[function_component] pub fn ConfigView() -> Html { let translate = use_translation(); + let services_ctx = use_service_context(); + let config_ctx = use_context::().expect("ConfigContext not found"); + let active_tab = use_state(|| ConfigPage::Main); let edit_mode = use_state(|| false); let form_state = use_state(ConfigFormState::default); @@ -68,28 +97,29 @@ pub fn ConfigView() -> Html { let tabs = { let form_state = form_state.clone(); let translate = translate.clone(); - let edit_mode = edit_mode.clone(); + let set_edit_mode = edit_mode.clone(); - use_memo((form_state, edit_mode, translate), move |(forms, edit, translate)| { + use_memo((form_state, set_edit_mode, translate), move |(forms, edit, translate)| { let forms: &ConfigFormState = forms; let modified_pages = collect_modified!(forms, [ - main, api, schedules, video, messaging, web_ui, + main, api, log, schedules, video, messaging, web_ui, reverse_proxy, hd_homerun, proxy, ipcheck ]).iter() - .filter_map(|maybe_form| maybe_form.as_ref().map(config_form_to_config_page)) + .map(config_form_to_config_page) .collect::>(); let tab_configs = vec![ - (ConfigPage::Main, "LABEL.MAIN", html! { }, "MainConfig"), - (ConfigPage::Api, "LABEL.API", html! { }, "ApiConfig"), - (ConfigPage::Schedules, "LABEL.SCHEDULES", html! { }, "SchedulesConfig"), - (ConfigPage::Messaging, "LABEL.MESSAGING", html! { }, "MessagingConfig"), - (ConfigPage::WebUi, "LABEL.WEB_UI", html! { }, "WebUiConfig"), - (ConfigPage::ReverseProxy, "LABEL.REVERSE_PROXY", html! { }, "ReverseProxyConfig"), - (ConfigPage::HdHomerun, "LABEL.HDHOMERUN_CONFIG", html! { }, "HdHomerunConfig"), - (ConfigPage::Proxy, "LABEL.PROXY", html! { }, "ProxyConfig"), - (ConfigPage::IpCheck, "LABEL.IP_CHECK", html! { }, "IpCheckConfig"), - (ConfigPage::Video, "LABEL.VIDEO", html! { }, "VideoConfig"), + (ConfigPage::Main, LABEL_MAIN, html! { }, "MainConfig"), + (ConfigPage::Api, LABEL_API, html! { }, "ApiConfig"), + (ConfigPage::Log, LABEL_LOG, html! { }, "Log"), + (ConfigPage::Schedules, LABEL_SCHEDULES, html! { }, "SchedulesConfig"), + (ConfigPage::Messaging, LABEL_MESSAGING, html! { }, "MessagingConfig"), + (ConfigPage::WebUi, LABEL_WEB_UI, html! { }, "WebUiConfig"), + (ConfigPage::ReverseProxy, LABEL_REVERSE_PROXY, html! { }, "ReverseProxyConfig"), + (ConfigPage::HdHomerun, LABEL_HDHOMERUN_CONFIG, html! { }, "HdHomerunConfig"), + (ConfigPage::Proxy, LABEL_PROXY, html! { }, "ProxyConfig"), + (ConfigPage::IpCheck, LABEL_IP_CHECK, html! { }, "IpCheckConfig"), + (ConfigPage::Video, LABEL_VIDEO, html! { }, "VideoConfig"), ]; let editing = **edit; @@ -115,14 +145,49 @@ pub fn ConfigView() -> Html { }; let handle_save_config = { + let config_ctx = config_ctx.clone(); + let translate = translate.clone(); + let services = services_ctx.clone(); let get_form_state = form_state.clone(); + let set_edit_mode = edit_mode.clone(); + Callback::from(move |_| { let forms = &*get_form_state; - let modified = collect_modified!(forms, [ - main, api, schedules, video, messaging, web_ui, - reverse_proxy, hd_homerun, proxy, ipcheck - ]); - warn!("Modified: {modified:?}"); + let modified_forms: Vec = collect_modified!(forms, [ + main, api, log, schedules, video, messaging, web_ui, + reverse_proxy, hd_homerun, proxy, ipcheck + ]); + + if !modified_forms.is_empty() { + let mut config_dto = config_ctx.config.as_ref().map_or_else(ConfigDto::default, + |app_cfg| app_cfg.config.clone()); + update_config(&mut config_dto, modified_forms); + match config_dto.prepare() { + Ok(_) => { + let services = services.clone(); + let translate = translate.clone(); + let set_edit_mode = set_edit_mode.clone(); + spawn_local(async move { + match services.config.save_config(config_dto).await{ + Ok(()) => { + services.toastr.success(translate.t("MESSAGES.SAVE.MAIN_CONFIG.SUCCESS")); + set_edit_mode.set(false); + let _cfg = services.config.get_server_config().await; + }, + Err(err) => { + services.toastr.error(translate.t("MESSAGES.SAVE.MAIN_CONFIG.FAIL")); + services.toastr.error(err.to_string()); + } + } + }); + } + Err(err) => { + services.toastr.error(err.to_string()); + } + } + } else { + set_edit_mode.set(false); + } }) }; @@ -134,6 +199,7 @@ pub fn ConfigView() -> Html { match form_data { ConfigForm::Main(_, _) => new_state.main = Some(form_data), ConfigForm::Api(_, _) => new_state.api = Some(form_data), + ConfigForm::Log(_, _) => new_state.log = Some(form_data), ConfigForm::Schedules(_, _) => new_state.schedules = Some(form_data), ConfigForm::Video(_, _) => new_state.video = Some(form_data), ConfigForm::Messaging(_, _) => new_state.messaging = Some(form_data), @@ -157,11 +223,11 @@ pub fn ConfigView() -> Html { context={context}>
-

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

+

{ translate.t(LABEL_CONFIG) }

@@ -174,7 +240,7 @@ pub fn ConfigView() -> Html {
})} diff --git a/frontend/src/app/components/config/hdhomerun_config_view.rs b/frontend/src/app/components/config/hdhomerun_config_view.rs index 11eb51753..a9d8e53fe 100644 --- a/frontend/src/app/components/config/hdhomerun_config_view.rs +++ b/frontend/src/app/components/config/hdhomerun_config_view.rs @@ -9,6 +9,11 @@ use crate::app::components::config::hdhomerun_device_view::{HdHomerunDeviceView} use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::HasFormData; +const LABEL_ENABLED: &str = "LABEL.ENABLED"; +const LABEL_DEVICE_AUTH: &str = "LABEL.DEVICE_AUTH"; +const LABEL_DEVICES: &str ="LABEL.DEVICES"; + + generate_form_reducer!( state: HdHomeRunConfigFormState { form: HdHomeRunConfigDto }, action_name: HdHomeRunConfigFormAction, @@ -54,22 +59,48 @@ pub fn HdHomerunConfigView() -> Html { }); } + let handle_device_change = { + let form_state = form_state.clone(); + Callback::from(move |(device_idx, _device_modified, device)| { + let mut devices = form_state.form.devices.clone(); + if let Some(slot) = devices.get_mut(device_idx) { + *slot = device; + } + form_state.dispatch(HdHomeRunConfigFormAction::Devices(devices)); + }) + }; + let handle_add_device = { let form_state = form_state.clone(); Callback::from(move |_| { - let mut new_state = form_state.data().clone(); + let mut devices = form_state.form.devices.clone(); let mut new_device = HdHomeRunDeviceConfigDto::default(); - new_device.prepare(new_state.devices.len() as u8); - new_state.devices.push(new_device); - form_state.dispatch(HdHomeRunConfigFormAction::SetAll(new_state)); + let next_port = devices.iter() + .map(|d| d.port) + .max() + .unwrap_or(8901) + 1; + new_device.port = next_port; + new_device.name = format!("hdhr_{next_port}"); + new_device.prepare(devices.len() as u8); + devices.push(new_device); + form_state.dispatch(HdHomeRunConfigFormAction::Devices(devices)); }) }; + let handle_remove_device = { + let form_state = form_state.clone(); + Callback::from(move |device_idx| { + let mut devices = form_state.form.devices.clone(); + devices.remove(device_idx); + form_state.dispatch(HdHomeRunConfigFormAction::Devices(devices)); + }) + }; + let render_empty = || { html! {
-

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

+

{translate.t(LABEL_DEVICES)}

@@ -82,29 +113,29 @@ pub fn HdHomerunConfigView() -> Html { if devices.is_empty() { render_empty() } else { - html!{ for devices.iter().map(|entry| html! { - + html!{ for devices.iter().enumerate().map(|(idx, entry)| html! { + })} } }; - let hdhomerun = form_state.data(); html! {
{if edit_mode { html! { <> - { edit_field_bool!(form_state, translate.t("LABEL.ENABLED"), enabled, HdHomeRunConfigFormAction::Enabled) } - { edit_field_bool!(form_state, translate.t("LABEL.DEVICE_AUTH"), auth, HdHomeRunConfigFormAction::Auth) } + { edit_field_bool!(form_state, translate.t(LABEL_ENABLED), enabled, HdHomeRunConfigFormAction::Enabled) } + { edit_field_bool!(form_state, translate.t(LABEL_DEVICE_AUTH), auth, HdHomeRunConfigFormAction::Auth) } } } else { html! { - <> - { config_field_bool!(hdhomerun, translate.t("LABEL.ENABLED"), enabled) } - { config_field_bool!(hdhomerun, translate.t("LABEL.DEVICE_AUTH"), auth) } - + <> + { config_field_bool!(&form_state.form, translate.t(LABEL_ENABLED), enabled) } + { config_field_bool!(&form_state.form, translate.t(LABEL_DEVICE_AUTH), auth) } + } } } @@ -115,7 +146,7 @@ pub fn HdHomerunConfigView() -> Html {
})}
- {render_devices(&hdhomerun.devices)} + {render_devices(&form_state.form.devices)}
} diff --git a/frontend/src/app/components/config/hdhomerun_device_view.rs b/frontend/src/app/components/config/hdhomerun_device_view.rs index 814f3a7dc..6c06f1e54 100644 --- a/frontend/src/app/components/config/hdhomerun_device_view.rs +++ b/frontend/src/app/components/config/hdhomerun_device_view.rs @@ -1,12 +1,34 @@ use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::HdHomeRunDeviceConfigDto; -use crate::app::components::{Card}; -use crate::{config_field}; +use shared::model::{HdHomeRunDeviceConfigDto}; +use crate::app::components::{Card, CollapsePanel, TextButton}; +use crate::app::components::config::macros::HasFormData; +use crate::{config_field, edit_field_number_u16, edit_field_number_u8, edit_field_text, generate_form_reducer, html_if}; + +generate_form_reducer!( + state: HdHomeRunDeviceConfigFormState { form: HdHomeRunDeviceConfigDto }, + action_name: HdHomeRunDeviceConfigFormAction, + fields { + FriendlyName => friendly_name: String, + Manufacturer => manufacturer: String, + ModelName => model_name: String, + ModelNumber => model_number: String, + FirmwareName => firmware_name: String, + FirmwareVersion => firmware_version: String, + DeviceType => device_type: String, + DeviceUdn => device_udn: String, + Name => name: String, + Port => port: u16, + TunerCount => tuner_count: u8, + } +); #[derive(Properties, PartialEq)] pub struct HdHomerunDeviceViewProps { + pub device_id: usize, // this is a transient Id to identify the dto in parent. pub device: HdHomeRunDeviceConfigDto, + pub on_form_change: Callback<(usize, bool, HdHomeRunDeviceConfigDto)>, + pub on_remove: Callback, #[prop_or(false)] pub edit_mode: bool, } @@ -15,33 +37,74 @@ pub struct HdHomerunDeviceViewProps { pub fn HdHomerunDeviceView(props: &HdHomerunDeviceViewProps) -> Html { let translate = use_translation(); - let device = use_state(HdHomeRunDeviceConfigDto::default); + let form_state: UseReducerHandle = use_reducer(|| { + HdHomeRunDeviceConfigFormState { form: props.device.clone(), modified: false } + }); { - let set_device = device.clone(); - use_effect_with(props.device.clone(), move |device| { - set_device.set(device.clone()); - }) + let on_form_change = props.on_form_change.clone(); + let device_id = props.device_id; + let deps = (form_state.clone(), form_state.modified); + use_effect_with(deps, move |(state, modified)| { + on_form_change.emit((device_id, *modified, state.form.clone())); + }); } - let render_device = |device: &HdHomeRunDeviceConfigDto| -> Html { + let handle_remove_device = { + let device_id = props.device_id; + let onremove = props.on_remove.clone(); + Callback::from(move |_| { + onremove.emit(device_id); + }) + }; + + let render_device = |device_state: &UseReducerHandle| -> Html { html! { -

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

- {config_field!(device, translate.t("LABEL.FRIENDLY_NAME"), friendly_name)} - {config_field!(device, translate.t("LABEL.MANUFACTURER"), manufacturer)} - {config_field!(device, translate.t("LABEL.MODEL_NAME"), model_name)} - {config_field!(device, translate.t("LABEL.MODEL_NUMBER"), model_number)} - {config_field!(device, translate.t("LABEL.FIRMWARE_NAME"), firmware_name)} - {config_field!(device, translate.t("LABEL.FIRMWARE_VERSION"), firmware_version)} - {config_field!(device, translate.t("LABEL.DEVICE_TYPE"), device_type)} - {config_field!(device, translate.t("LABEL.DEVICE_UDN"), device_udn)} - {config_field!(device, translate.t("LABEL.NAME"), name)} - {config_field!(device, translate.t("LABEL.PORT"), port)} - {config_field!(device, translate.t("LABEL.TUNER_COUNT"), tuner_count)} +
+

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

+ {html_if!(props.edit_mode, { + + })} +
+ if props.edit_mode { + {edit_field_text!(device_state, translate.t("LABEL.NAME"), name, HdHomeRunDeviceConfigFormAction::Name)} + {edit_field_number_u16!(device_state, translate.t("LABEL.PORT"), port, HdHomeRunDeviceConfigFormAction::Port)} + {edit_field_number_u8!(device_state, translate.t("LABEL.TUNER_COUNT"), tuner_count, HdHomeRunDeviceConfigFormAction::TunerCount)} + {edit_field_text!(device_state, translate.t("LABEL.DEVICE_UDN"), device_udn, HdHomeRunDeviceConfigFormAction::DeviceUdn)} + + <> + {edit_field_text!(device_state, translate.t("LABEL.FRIENDLY_NAME"), friendly_name, HdHomeRunDeviceConfigFormAction::FriendlyName)} + {edit_field_text!(device_state, translate.t("LABEL.MANUFACTURER"), manufacturer, HdHomeRunDeviceConfigFormAction::Manufacturer)} + {edit_field_text!(device_state, translate.t("LABEL.MODEL_NAME"), model_name, HdHomeRunDeviceConfigFormAction::ModelName)} + {edit_field_text!(device_state, translate.t("LABEL.MODEL_NUMBER"), model_number, HdHomeRunDeviceConfigFormAction::ModelNumber)} + {edit_field_text!(device_state, translate.t("LABEL.FIRMWARE_NAME"), firmware_name, HdHomeRunDeviceConfigFormAction::FirmwareName)} + {edit_field_text!(device_state, translate.t("LABEL.FIRMWARE_VERSION"), firmware_version, HdHomeRunDeviceConfigFormAction::FirmwareVersion)} + {edit_field_text!(device_state, translate.t("LABEL.DEVICE_TYPE"), device_type, HdHomeRunDeviceConfigFormAction::DeviceType)} + + + } else { + {config_field!(&device_state.form, translate.t("LABEL.NAME"), name)} + {config_field!(&device_state.form, translate.t("LABEL.PORT"), port)} + {config_field!(&device_state.form, translate.t("LABEL.TUNER_COUNT"), tuner_count)} + {config_field!(&device_state.form, translate.t("LABEL.DEVICE_UDN"), device_udn)} + + <> + {config_field!(&device_state.form, translate.t("LABEL.FRIENDLY_NAME"), friendly_name)} + {config_field!(&device_state.form, translate.t("LABEL.MANUFACTURER"), manufacturer)} + {config_field!(&device_state.form, translate.t("LABEL.MODEL_NAME"), model_name)} + {config_field!(&device_state.form, translate.t("LABEL.MODEL_NUMBER"), model_number)} + {config_field!(&device_state.form, translate.t("LABEL.FIRMWARE_NAME"), firmware_name)} + {config_field!(&device_state.form, translate.t("LABEL.FIRMWARE_VERSION"), firmware_version)} + {config_field!(&device_state.form, translate.t("LABEL.DEVICE_TYPE"), device_type)} + + + }
} }; - render_device(&device) + render_device(&form_state) } \ No newline at end of file diff --git a/frontend/src/app/components/config/ipcheck_config_view.rs b/frontend/src/app/components/config/ipcheck_config_view.rs index 765d63198..218beeb51 100644 --- a/frontend/src/app/components/config/ipcheck_config_view.rs +++ b/frontend/src/app/components/config/ipcheck_config_view.rs @@ -48,7 +48,7 @@ pub fn IpCheckConfigView() -> Html { let ipcheck_config = config_ctx .config .as_ref() - .and_then(|c| c.config.ipcheck.clone()); // clone() Option + .and_then(|c| c.config.ipcheck.clone()); use_effect_with((ipcheck_config, config_view_ctx.edit_mode.clone()), move |(ipcheck_cfg, _mode)| { if let Some(ipcheck) = ipcheck_cfg { diff --git a/frontend/src/app/components/config/log_config_view.rs b/frontend/src/app/components/config/log_config_view.rs new file mode 100644 index 000000000..a9a5e4e27 --- /dev/null +++ b/frontend/src/app/components/config/log_config_view.rs @@ -0,0 +1,120 @@ +use std::rc::Rc; +use yew::prelude::*; +use yew_i18n::use_translation; +use shared::model::{LogConfigDto}; +use crate::app::context::ConfigContext; +use crate::{config_field_bool, config_field_child, edit_field_bool, generate_form_reducer}; +use crate::app::components::config::config_view_context::ConfigViewContext; +use crate::app::components::config::config_page::{ConfigForm}; +use crate::app::components::config::macros::HasFormData; +use crate::app::components::{Chip, RadioButtonGroup}; + +const LABEL_LOG_LEVEL: &str = "LABEL.LOG_LEVEL"; +const LABEL_LOG_ACTIVE_USER: &str = "LABEL.LOG_ACTIVE_USER"; +const LABEL_LOG_SANITIZE_SENSITIVE_INFO: &str = "LABEL.SANITIZE_SENSITIVE_INFO"; + +const LOG_LEVELS: [&str; 5] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]; + +generate_form_reducer!( + state: LogConfigFormState { form: LogConfigDto }, + action_name: LogConfigFormAction, + fields { + LogLevel => log_level: Option, + SanitizeSensitiveInfo => sanitize_sensitive_info: bool, + LogActiveUser => log_active_user: bool, + } +); + +#[function_component] +pub fn LogConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("ConfigContext not found"); + let config_view_ctx = use_context::().expect("ConfigViewContext not found"); + + let log_level_options = use_memo((), |_| { + LOG_LEVELS.iter().map(ToString::to_string).collect::>() + }); + + let form_state: UseReducerHandle = use_reducer(|| { + LogConfigFormState { form: LogConfigDto::default(), modified: false } + }); + + { + let on_form_change = config_view_ctx.on_form_change.clone(); + let deps = (form_state.clone(), form_state.modified); + use_effect_with(deps, move |(state, modified)| { + on_form_change.emit(ConfigForm::Log(*modified, state.form.clone())); + }); + } + + { + let form_state = form_state.clone(); + let log_config = config_ctx + .config + .as_ref() + .and_then(|c| c.config.log.clone()); // clone() Option + + use_effect_with((log_config, config_view_ctx.edit_mode.clone()), move |(log_cfg, _mode)| { + if let Some(log) = log_cfg { + form_state.dispatch(LogConfigFormAction::SetAll((*log).clone())); + } else { + form_state.dispatch(LogConfigFormAction::SetAll(LogConfigDto::default())); + } + || () + }); + } + + let render_view_mode = || { + let log_state = form_state.clone(); + html! { + <> +
+ { config_field_child!(translate.t(LABEL_LOG_LEVEL), { + match log_state.form.log_level.as_ref() { + Some(level) => html! { }, + None => html! { }, + } + })} +
+ { config_field_bool!(log_state.form, translate.t(LABEL_LOG_ACTIVE_USER), log_active_user) } + { config_field_bool!(log_state.form, translate.t(LABEL_LOG_SANITIZE_SENSITIVE_INFO), sanitize_sensitive_info) } + + } + }; + + let render_edit_mode = || { + let forms = form_state.clone(); + let log_level_selection = Rc::new(forms.form.log_level.as_ref().map_or_else(Vec::new, |l| vec![l.to_uppercase()])); + html! { + <> + { config_field_child!(translate.t(LABEL_LOG_LEVEL), { + html! { >| { + let level: Option = selections.first().map(ToString::to_string); + forms.dispatch(LogConfigFormAction::LogLevel(level)); + })} + options={log_level_options.clone()} + selected={log_level_selection} + /> + }})} + { edit_field_bool!(form_state, translate.t(LABEL_LOG_ACTIVE_USER), log_active_user, LogConfigFormAction::LogActiveUser) } + { edit_field_bool!(form_state, translate.t(LABEL_LOG_SANITIZE_SENSITIVE_INFO), sanitize_sensitive_info, LogConfigFormAction::SanitizeSensitiveInfo) } + + } + }; + + html! { +
+
+ { + if *config_view_ctx.edit_mode { + render_edit_mode() + } else { + render_view_mode() + } + } +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/main_config_view.rs b/frontend/src/app/components/config/main_config_view.rs index 48aa3ad91..a728b4784 100644 --- a/frontend/src/app/components/config/main_config_view.rs +++ b/frontend/src/app/components/config/main_config_view.rs @@ -5,7 +5,7 @@ use crate::app::context::ConfigContext; use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::ConfigForm; use crate::app::components::config::macros::HasFormData; -use crate::{config_field_empty, config_field_optional, config_field_bool_empty, config_field_bool, config_field, edit_field_text_option, edit_field_bool, generate_form_reducer, edit_field_number, edit_field_number_option, edit_field_text, edit_field_number_u8}; +use crate::{config_field_optional, config_field_bool, config_field, edit_field_text_option, edit_field_bool, generate_form_reducer, edit_field_number, edit_field_number_option, edit_field_text, edit_field_number_u8}; const LABEL_UPDATE_ON_BOOT: &str = "LABEL.UPDATE_ON_BOOT"; const LABEL_CONFIG_HOT_RELOAD: &str = "LABEL.CONFIG_HOT_RELOAD"; @@ -69,41 +69,21 @@ pub fn MainConfigView() -> Html { }); } - let render_empty = || html! { - <> - { config_field_bool_empty!(translate.t(LABEL_UPDATE_ON_BOOT)) } - { config_field_bool_empty!(translate.t(LABEL_CONFIG_HOT_RELOAD)) } - { config_field_bool_empty!(translate.t(LABEL_USER_ACCESS_CONTROL)) } - { config_field_empty!(translate.t(LABEL_THREADS)) } - { config_field_empty!(translate.t(LABEL_WORKING_DIR)) } - { config_field_empty!(translate.t(LABEL_MAPPING_PATH)) } - { config_field_empty!(translate.t(LABEL_BACKUP_DIR)) } - { config_field_empty!(translate.t(LABEL_USER_CONFIG_DIR)) } - { config_field_empty!(translate.t(LABEL_SLEEP_TIMER_MINS)) } - { config_field_empty!(translate.t(LABEL_CONNECT_TIMEOUT_SECS)) } - { config_field_empty!(translate.t(LABEL_CUSTOM_STREAM_RESPONSE_PATH)) } - - }; - let render_view_mode = || { - if let Some(config) = &config_ctx.config { - html! { - <> - { config_field_bool!(config.config, translate.t(LABEL_UPDATE_ON_BOOT), update_on_boot) } - { config_field_bool!(config.config, translate.t(LABEL_CONFIG_HOT_RELOAD), config_hot_reload) } - { config_field_bool!(config.config, translate.t(LABEL_USER_ACCESS_CONTROL), user_access_control) } - { config_field!(config.config, translate.t(LABEL_THREADS), threads) } - { config_field!(config.config, translate.t(LABEL_WORKING_DIR), working_dir) } - { config_field_optional!(config.config, translate.t(LABEL_MAPPING_PATH), mapping_path) } - { config_field_optional!(config.config, translate.t(LABEL_BACKUP_DIR), backup_dir) } - { config_field_optional!(config.config, translate.t(LABEL_USER_CONFIG_DIR), user_config_dir) } - { config_field_optional!(config.config, translate.t(LABEL_SLEEP_TIMER_MINS), sleep_timer_mins) } - { config_field!(config.config, translate.t(LABEL_CONNECT_TIMEOUT_SECS), connect_timeout_secs) } - { config_field_optional!(config.config, translate.t(LABEL_CUSTOM_STREAM_RESPONSE_PATH), custom_stream_response_path) } - - } - } else { - render_empty() + html! { + <> + { config_field_bool!(form_state.form, translate.t(LABEL_UPDATE_ON_BOOT), update_on_boot) } + { config_field_bool!(form_state.form, translate.t(LABEL_CONFIG_HOT_RELOAD), config_hot_reload) } + { config_field_bool!(form_state.form, translate.t(LABEL_USER_ACCESS_CONTROL), user_access_control) } + { config_field!(form_state.form, translate.t(LABEL_THREADS), threads) } + { config_field!(form_state.form, translate.t(LABEL_WORKING_DIR), working_dir) } + { config_field_optional!(form_state.form, translate.t(LABEL_MAPPING_PATH), mapping_path) } + { config_field_optional!(form_state.form, translate.t(LABEL_BACKUP_DIR), backup_dir) } + { config_field_optional!(form_state.form, translate.t(LABEL_USER_CONFIG_DIR), user_config_dir) } + { config_field_optional!(form_state.form, translate.t(LABEL_SLEEP_TIMER_MINS), sleep_timer_mins) } + { config_field!(form_state.form, translate.t(LABEL_CONNECT_TIMEOUT_SECS), connect_timeout_secs) } + { config_field_optional!(form_state.form, translate.t(LABEL_CUSTOM_STREAM_RESPONSE_PATH), custom_stream_response_path) } + } }; diff --git a/frontend/src/app/components/config/messaging_config_view.rs b/frontend/src/app/components/config/messaging_config_view.rs index aadaf6f81..643c95423 100644 --- a/frontend/src/app/components/config/messaging_config_view.rs +++ b/frontend/src/app/components/config/messaging_config_view.rs @@ -7,10 +7,7 @@ use crate::{ config_field, config_field_child, config_field_empty, config_field_hide, config_field_optional, edit_field_list, edit_field_text, edit_field_text_option, generate_form_reducer, }; -use shared::model::{ - MessagingConfigDto, MsgKind, PushoverMessagingConfigDto, RestMessagingConfigDto, - TelegramMessagingConfigDto, -}; +use shared::model::{MessagingConfigDto, MsgKind, PushoverMessagingConfigDto, RestMessagingConfigDto, TelegramMessagingConfigDto}; use std::rc::Rc; use std::str::FromStr; use yew::prelude::*; @@ -92,13 +89,17 @@ pub fn MessagingConfigView() -> Html { let notify_on_options = use_memo((), |_| { vec![ - MsgKind::Info.to_string(), - MsgKind::Stats.to_string(), - MsgKind::Error.to_string(), - MsgKind::Watch.to_string(), + MsgKind::Info, + MsgKind::Stats, + MsgKind::Error, + MsgKind::Watch, ] }); + let notify_on_options_text = use_memo(notify_on_options.clone(), |options| { + options.iter().map(ToString::to_string).collect::>() + }); + { let on_form_change = config_view_ctx.on_form_change.clone(); let messaging_state = messaging_state.clone(); @@ -127,6 +128,43 @@ pub fn MessagingConfigView() -> Html { }); } + + { + let msg_state = messaging_state.clone(); + let t_state = telegram_state.clone(); + let p_state = pushover_state.clone(); + let r_state = rest_state.clone(); + + let msg_config : MessagingConfigDto = config_ctx + .config + .as_ref() + .and_then(|c| c.config.messaging.as_ref()) + .map_or_else(MessagingConfigDto::default, |m| m.clone()); + + let telegram_cfg = msg_config.telegram.as_ref().map_or_else(TelegramMessagingConfigDto::default, |t| t.clone()); + use_effect_with((telegram_cfg, config_view_ctx.edit_mode.clone()), move |(telegram_cfg, _mode)| { + t_state.dispatch(TelegramMessagingConfigFormAction::SetAll(telegram_cfg.clone())); + || () + }); + + let rest_cfg = msg_config.rest.as_ref().map_or_else(RestMessagingConfigDto::default, |t| t.clone()); + use_effect_with((rest_cfg, config_view_ctx.edit_mode.clone()), move |(rest_cfg, _mode)| { + r_state.dispatch(RestMessagingConfigFormAction::SetAll(rest_cfg.clone())); + || () + }); + + let pushover_cfg = msg_config.pushover.as_ref().map_or_else(PushoverMessagingConfigDto::default, |t| t.clone()); + use_effect_with((pushover_cfg, config_view_ctx.edit_mode.clone()), move |(pushover_cfg, _mode)| { + p_state.dispatch(PushoverMessagingConfigFormAction::SetAll(pushover_cfg.clone())); + || () + }); + + use_effect_with((msg_config, config_view_ctx.edit_mode.clone()), move |(msg_config, _mode)| { + msg_state.dispatch(MessagingConfigFormAction::SetAll(msg_config.clone())); + || () + }); + } + let render_telegram = |telegram: Option<&TelegramMessagingConfigDto>| match telegram { Some(entry) => html! { @@ -190,45 +228,29 @@ pub fn MessagingConfigView() -> Html { }, }; - let render_empty = || { + let render_view_mode = || { + let msg_state = messaging_state.clone(); html! { <> -
- { config_field_empty!(translate.t(LABEL_NOTIFY_ON)) } -
-
- {render_telegram(None)} - {render_rest(None)} - {render_pushover(None)} -
- - } - }; - - let render_view_mode = || { - if let Some(config) = &config_ctx.config { - if let Some(messaging) = &config.config.messaging { - html! { - <> -
- { config_field_child!(translate.t(LABEL_NOTIFY_ON), { - html! {
- { for messaging.notify_on.iter().map(|t| html! { }) } -
} - })} +
+ { config_field_child!(translate.t(LABEL_NOTIFY_ON), { + html! {
+ { for notify_on_options.iter().map(|t| { + let is_selected = msg_state.form.notify_on.contains(t); + let class = if is_selected { "tp__text-button primary" } else { "tp__text-button" }; + html! { + + }}) }
-
- {render_telegram(messaging.telegram.as_ref())} - {render_rest(messaging.rest.as_ref())} - {render_pushover(messaging.pushover.as_ref())} -
- - } - } else { - render_empty() - } - } else { - render_empty() + } + })} +
+
+ {render_telegram(msg_state.form.telegram.as_ref())} + {render_rest(msg_state.form.rest.as_ref())} + {render_pushover(msg_state.form.pushover.as_ref())} +
+ } }; @@ -252,7 +274,7 @@ pub fn MessagingConfigView() -> Html { msg_state.dispatch(MessagingConfigFormAction::NotifyOn( selections.iter().filter_map(|s| MsgKind::from_str(s).ok()).collect())); })} - options={notify_on_options.clone()} + options={notify_on_options_text.clone()} selected={notify_on_selections} /> }})} diff --git a/frontend/src/app/components/config/mod.rs b/frontend/src/app/components/config/mod.rs index 0120f2f55..0c5778e08 100644 --- a/frontend/src/app/components/config/mod.rs +++ b/frontend/src/app/components/config/mod.rs @@ -14,6 +14,8 @@ mod video_config_view; mod config_view_context; mod config_page; mod hdhomerun_device_view; +mod config_update; +mod log_config_view; pub use config_view::*; pub use main_config_view::*; @@ -26,4 +28,5 @@ pub use hdhomerun_config_view::*; pub use proxy_config_view::*; pub use ipcheck_config_view::*; pub use video_config_view::*; +pub use log_config_view::*; pub use macros::*; \ No newline at end of file diff --git a/frontend/src/app/components/config/proxy_config_view.rs b/frontend/src/app/components/config/proxy_config_view.rs index 2e2bf23b1..3a7d04089 100644 --- a/frontend/src/app/components/config/proxy_config_view.rs +++ b/frontend/src/app/components/config/proxy_config_view.rs @@ -6,7 +6,7 @@ use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::ConfigForm; use crate::app::components::config::macros::HasFormData; use crate::{ - config_field, config_field_empty, config_field_optional, config_field_optional_hide, + config_field, config_field_optional, config_field_optional_hide, edit_field_text, edit_field_text_option, generate_form_reducer, }; @@ -55,29 +55,13 @@ pub fn ProxyConfigView() -> Html { }); } - let render_empty = || html! { -
- { config_field_empty!(translate.t(LABEL_URL)) } - { config_field_empty!(translate.t(LABEL_USERNAME)) } - { config_field_empty!(translate.t(LABEL_PASSWORD)) } -
- }; - let render_view_mode = || { - if let Some(config) = &config_ctx.config { - if let Some(proxy) = &config.config.proxy { - html! { -
- { config_field!(proxy, translate.t(LABEL_URL), url) } - { config_field_optional!(proxy, translate.t(LABEL_USERNAME), username) } - { config_field_optional_hide!(proxy, translate.t(LABEL_PASSWORD), password) } -
- } - } else { - render_empty() - } - } else { - render_empty() + html! { +
+ { 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) } +
} }; diff --git a/frontend/src/app/components/config/reverse_proxy_config_view.rs b/frontend/src/app/components/config/reverse_proxy_config_view.rs index 248de5189..a1149c4b3 100644 --- a/frontend/src/app/components/config/reverse_proxy_config_view.rs +++ b/frontend/src/app/components/config/reverse_proxy_config_view.rs @@ -8,7 +8,7 @@ use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::ConfigForm; use crate::app::components::config::macros::HasFormData; use crate::app::components::Card; -use crate::{config_field, config_field_bool, config_field_bool_empty, config_field_empty, config_field_optional, +use crate::{config_field, config_field_bool, config_field_optional, edit_field_bool, edit_field_number, edit_field_number_u64, edit_field_text_option, generate_form_reducer}; const LABEL_CACHE: &str = "LABEL.CACHE"; @@ -136,112 +136,54 @@ pub fn ReverseProxyConfigView() -> Html { }); } - let render_cache = |config: Option<&CacheConfigDto>| { - match config { - Some(entry) => html! { - -

{translate.t(LABEL_CACHE)}

- { config_field_bool!(entry, translate.t(LABEL_ENABLED), enabled) } - { config_field_optional!(entry, translate.t(LABEL_SIZE), size) } - { config_field_optional!(entry, translate.t(LABEL_DIRECTORY), dir) } -
- }, - None => html! { - -

{translate.t(LABEL_CACHE)}

- { config_field_empty!(translate.t(LABEL_ENABLED)) } - { config_field_empty!(translate.t(LABEL_SIZE)) } - { config_field_empty!(translate.t(LABEL_DIRECTORY)) } -
- }, - } - }; - let render_stream = |config: Option<&StreamConfigDto>| { - //pub buffer: Option, - match config { - Some(entry) => html! { - -

{translate.t(LABEL_STREAM)}

- { config_field_bool!(entry, translate.t(LABEL_RETRY), retry) } - { config_field_optional!(entry, translate.t(LABEL_THROTTLE), throttle) } - { config_field!(entry, translate.t(LABEL_GRACE_PERIOD_MILLIS), grace_period_millis) } - { config_field!(entry, translate.t(LABEL_GRACE_PERIOD_TIMEOUT_SECS), grace_period_timeout_secs) } - { config_field!(entry, translate.t(LABEL_FORCED_RETRY_INTERVAL_SECS), forced_retry_interval_secs) } - { config_field!(entry, translate.t(LABEL_THROTTLE_KBPS), throttle_kbps) } -
- }, - None => html! { - -

{translate.t(LABEL_STREAM)}

- { config_field_empty!(translate.t(LABEL_RETRY)) } - { config_field_empty!(translate.t(LABEL_THROTTLE)) } - { config_field_empty!(translate.t(LABEL_GRACE_PERIOD_MILLIS)) } - { config_field_empty!(translate.t(LABEL_GRACE_PERIOD_TIMEOUT_SECS)) } - { config_field_empty!(translate.t(LABEL_FORCED_RETRY_INTERVAL_SECS)) } - { config_field_empty!(translate.t(LABEL_THROTTLE_KBPS)) } -
- }, - } - }; - - let render_rate_limit = |config: Option<&RateLimitConfigDto>| { - match config { - Some(entry) => html! { - -

{translate.t(LABEL_RATE_LIMIT)}

- { config_field_bool!(entry, translate.t(LABEL_ENABLED), enabled) } - { config_field!(entry, translate.t(LABEL_PERIOD_MILLIS), period_millis) } - { config_field!(entry, translate.t(LABEL_BURST_SIZE), burst_size) } -
- }, - None => html! { - -

{translate.t(LABEL_RATE_LIMIT)}

- { config_field_empty!(translate.t(LABEL_ENABLED)) } - { config_field_empty!(translate.t(LABEL_PERIOD_MILLIS)) } - { config_field_empty!(translate.t(LABEL_BURST_SIZE)) } -
- }, - } - }; - - let render_empty = || { + let render_cache = || { html! { - <> -
- { config_field_bool_empty!(translate.t(LABEL_RESOURCE_REWRITE_DISABLED)) } - { config_field_bool_empty!(translate.t(LABEL_DISABLE_REFERER_HEADER)) } -
-
- {render_cache(None)} - {render_rate_limit(None)} - {render_stream(None)} -
- + +

{translate.t(LABEL_CACHE)}

+ { config_field_bool!(cache_state.form, translate.t(LABEL_ENABLED), enabled) } + { config_field_optional!(cache_state.form, translate.t(LABEL_SIZE), size) } + { config_field_optional!(cache_state.form, translate.t(LABEL_DIRECTORY), dir) } +
+ } + }; + let render_stream = || { + html! { + +

{translate.t(LABEL_STREAM)}

+ { config_field_bool!(stream_state.form, translate.t(LABEL_RETRY), retry) } + { config_field_optional!(stream_state.form, translate.t(LABEL_THROTTLE), throttle) } + { config_field!(stream_state.form, translate.t(LABEL_GRACE_PERIOD_MILLIS), grace_period_millis) } + { config_field!(stream_state.form, translate.t(LABEL_GRACE_PERIOD_TIMEOUT_SECS), grace_period_timeout_secs) } + { config_field!(stream_state.form, translate.t(LABEL_FORCED_RETRY_INTERVAL_SECS), forced_retry_interval_secs) } + { config_field!(stream_state.form, translate.t(LABEL_THROTTLE_KBPS), throttle_kbps) } +
+ } + }; + + let render_rate_limit = || { + html! { + +

{translate.t(LABEL_RATE_LIMIT)}

+ { config_field_bool!(rate_limit_state.form, translate.t(LABEL_ENABLED), enabled) } + { config_field!(rate_limit_state.form, translate.t(LABEL_PERIOD_MILLIS), period_millis) } + { config_field!(rate_limit_state.form, translate.t(LABEL_BURST_SIZE), burst_size) } +
} }; let render_view_mode = || { - if let Some(config) = &config_ctx.config { - if let Some(reverse_proxy) = &config.config.reverse_proxy { - html! { - <> -
- { config_field_bool!(reverse_proxy, translate.t(LABEL_RESOURCE_REWRITE_DISABLED), resource_rewrite_disabled) } - { config_field_bool!(reverse_proxy, translate.t(LABEL_DISABLE_REFERER_HEADER), disable_referer_header) } -
-
- { render_cache(reverse_proxy.cache.as_ref()) } - { render_rate_limit(reverse_proxy.rate_limit.as_ref()) } - { render_stream(reverse_proxy.stream.as_ref()) } -
- - } - } else { - render_empty() - } - } else { - render_empty() + html! { + <> +
+ { config_field_bool!(reverse_proxy_state.form, translate.t(LABEL_RESOURCE_REWRITE_DISABLED), resource_rewrite_disabled) } + { config_field_bool!(reverse_proxy_state.form, translate.t(LABEL_DISABLE_REFERER_HEADER), disable_referer_header) } +
+
+ { render_cache() } + { render_rate_limit() } + { render_stream() } +
+ } }; diff --git a/frontend/src/app/components/config/video_config_view.rs b/frontend/src/app/components/config/video_config_view.rs index b564e41d5..a8dcfec1c 100644 --- a/frontend/src/app/components/config/video_config_view.rs +++ b/frontend/src/app/components/config/video_config_view.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use crate::app::components::{Card, Chip, KeyValueEditor}; use crate::app::context::ConfigContext; -use crate::{config_field_bool, config_field_child, config_field_empty, config_field_optional, edit_field_bool, edit_field_text_option, edit_field_list, generate_form_reducer, config_field_bool_empty}; +use crate::{config_field_bool, config_field_child, config_field_optional, edit_field_bool, edit_field_text_option, + edit_field_list, generate_form_reducer}; use shared::model::{VideoDownloadConfigDto, VideoConfigDto}; use yew::prelude::*; use yew_i18n::use_translation; @@ -100,65 +101,38 @@ pub fn VideoConfigView() -> Html { }; - let render_download_view = |download: Option<&VideoDownloadConfigDto>| html! { - match download { - Some(entry) => html! { + let render_download_view = || html! { + html! {

{translate.t(LABEL_DOWNLOAD)}

- { 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_bool!(download_state.form, translate.t(LABEL_ORGANIZE_INTO_DIRECTORIES), organize_into_directories) } + { config_field_optional!(download_state.form, translate.t(LABEL_DIRECTORY), directory) } + { config_field_optional!(download_state.form, translate.t(LABEL_EPISODE_PATTERN), episode_pattern) } { config_field_child!(translate.t(LABEL_HEADERS), { html! {
    - { for entry.headers.iter().map(|(k,v)| html!{
  • {k}{":"} {v}
  • }) } + { for download_state.form.headers.iter().map(|(k,v)| html!{
  • {k}{":"} {v}
  • }) }
} })}
- }, - None => html! { - -

{translate.t(LABEL_DOWNLOAD)}

- { config_field_bool_empty!(translate.t(LABEL_ORGANIZE_INTO_DIRECTORIES)) } - { config_field_empty!(translate.t(LABEL_DIRECTORY)) } - { config_field_empty!(translate.t(LABEL_EPISODE_PATTERN)) } - { config_field_empty!(translate.t(LABEL_HEADERS)) } -
- } } }; - let render_empty = || html! { - <> -
- { config_field_empty!(translate.t(LABEL_WEB_SEARCH)) } -
-
- { config_field_empty!(translate.t(LABEL_EXTENSIONS)) } - { render_download_view(None) } -
- - }; - let render_view_mode = || { - if let Some(config) = &config_ctx.config { - if let Some(video) = &config.config.video { - html! { - <> -
- { config_field_optional!(video, translate.t(LABEL_WEB_SEARCH), web_search) } -
-
- { render_extensions(&video.extensions) } - { render_download_view(video.download.as_ref()) } -
- - } - } else { render_empty() } - } else { render_empty() } + html! { + <> +
+ { config_field_optional!(video_state.form, translate.t(LABEL_WEB_SEARCH), web_search) } +
+
+ { render_extensions(&video_state.form.extensions) } + { render_download_view() } +
+ + } }; let render_edit_mode = || { diff --git a/frontend/src/app/components/config/webui_config_view.rs b/frontend/src/app/components/config/webui_config_view.rs index 82cd671a5..164c62ade 100644 --- a/frontend/src/app/components/config/webui_config_view.rs +++ b/frontend/src/app/components/config/webui_config_view.rs @@ -3,7 +3,7 @@ use crate::app::context::ConfigContext; use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::ConfigForm; use crate::app::components::config::macros::HasFormData; -use crate::{config_field, config_field_bool, config_field_bool_empty, config_field_child, config_field_empty, +use crate::{config_field, config_field_bool, config_field_child, config_field_hide, config_field_optional, edit_field_bool, edit_field_list_option, edit_field_number, edit_field_text, edit_field_text_option, generate_form_reducer}; use yew::prelude::*; @@ -121,99 +121,40 @@ pub fn WebUiConfigView() -> Html { }); } - // Empty renderings - let render_empty_auth = || { - html! { - -

{translate.t(LABEL_AUTH)}

- { config_field_empty!(translate.t(LABEL_ENABLED)) } - { config_field_empty!(translate.t(LABEL_ISSUER)) } - { config_field_empty!(translate.t(LABEL_SECRET)) } - { config_field_empty!(translate.t(LABEL_TOKEN_TTL_MINS)) } - { config_field_empty!(translate.t(LABEL_USERFILE)) } -
- } - }; - - let render_empty_csp = || { - html! { - -

{translate.t(LABEL_CONTENT_SECURITY_POLICY)}

- { config_field_bool_empty!(translate.t(LABEL_ENABLED)) } - { config_field_empty!(translate.t(LABEL_CONTENT_SECURITY_POLICY_CUSTOM_ATTRIBUTES)) } -
- } - }; - - let render_empty = || { - html! { - <> - { config_field_bool_empty!(translate.t(LABEL_ENABLED)) } - { config_field_bool_empty!(translate.t(LABEL_USER_UI_ENABLED)) } - { config_field_empty!(translate.t(LABEL_PATH)) } - { config_field_empty!(translate.t(LABEL_PLAYER_SERVER)) } - { render_empty_csp()} - { render_empty_auth() } - - } - }; - // View mode let render_view_mode = || { - if let Some(config) = &config_ctx.config { - if let Some(web_ui) = &config.config.web_ui { - html! { - <> - { config_field_bool!(web_ui, translate.t(LABEL_ENABLED), enabled) } - { config_field_bool!(web_ui, translate.t(LABEL_USER_UI_ENABLED), user_ui_enabled) } - { config_field_optional!(web_ui, translate.t(LABEL_PATH), path) } - { config_field_optional!(web_ui, translate.t(LABEL_PLAYER_SERVER), player_server) } - { - match web_ui.content_security_policy.as_ref() { - Some(csp) => html! { - -

{translate.t(LABEL_CONTENT_SECURITY_POLICY)}

- { config_field_bool!(csp, translate.t(LABEL_ENABLED), enabled) } - { config_field_child!(translate.t(LABEL_CONTENT_SECURITY_POLICY_CUSTOM_ATTRIBUTES), { - html! { -
- { - if let Some(custom) = &csp.custom_attributes { - html! { for custom.iter().map(|a| html! { }) } - } else { - html! {} - } - } -
- } - }) } -
- }, - None => render_empty_csp() - } + html! { + <> + { config_field_bool!(webui_state.form, translate.t(LABEL_ENABLED), enabled) } + { config_field_bool!(webui_state.form, translate.t(LABEL_USER_UI_ENABLED), user_ui_enabled) } + { config_field_optional!(webui_state.form, translate.t(LABEL_PATH), path) } + { config_field_optional!(webui_state.form, translate.t(LABEL_PLAYER_SERVER), player_server) } + +

{translate.t(LABEL_CONTENT_SECURITY_POLICY)}

+ { config_field_bool!(csp_state.form, translate.t(LABEL_ENABLED), enabled) } + { config_field_child!(translate.t(LABEL_CONTENT_SECURITY_POLICY_CUSTOM_ATTRIBUTES), { + html! { +
+ { + if let Some(custom) = &csp_state.form.custom_attributes { + html! { for custom.iter().map(|a| html! { }) } + } else { + html! {} + } + } +
} - { - match web_ui.auth.as_ref() { - Some(auth) => html!{ - -

{translate.t(LABEL_AUTH)}

- { config_field_bool!(auth, translate.t(LABEL_ENABLED), enabled) } - { config_field!(auth, translate.t(LABEL_ISSUER), issuer) } - { config_field_hide!(auth, translate.t(LABEL_SECRET), secret) } - { config_field!(auth, translate.t(LABEL_TOKEN_TTL_MINS), token_ttl_mins) } - { config_field_optional!(auth, translate.t(LABEL_USERFILE), userfile) } -
- }, - None => render_empty_auth(), - } - } - - } - } else { - render_empty() - } - } else { - render_empty() + }) } +
+ +

{translate.t(LABEL_AUTH)}

+ { config_field_bool!(auth_state.form, translate.t(LABEL_ENABLED), enabled) } + { config_field!(auth_state.form, translate.t(LABEL_ISSUER), issuer) } + { config_field_hide!(auth_state.form, translate.t(LABEL_SECRET), secret) } + { config_field!(auth_state.form, translate.t(LABEL_TOKEN_TTL_MINS), token_ttl_mins) } + { config_field_optional!(auth_state.form, translate.t(LABEL_USERFILE), userfile) } +
+ } }; diff --git a/frontend/src/app/components/dashboard/ipinfo_action_card.rs b/frontend/src/app/components/dashboard/ipinfo_action_card.rs index fdf98b235..babc5bd3d 100644 --- a/frontend/src/app/components/dashboard/ipinfo_action_card.rs +++ b/frontend/src/app/components/dashboard/ipinfo_action_card.rs @@ -69,18 +69,18 @@ pub fn IpinfoActionCard() -> Html { }); } - { - let services_ctx = services.clone(); - let config_exists_state = config_exists.clone(); - let _ = use_future(|| async move { - let cfg = services_ctx.config.get_server_config().await; - config_exists_state.set(if let Some(app_cfg) = &cfg { - app_cfg.config.ipcheck.is_some() - } else { - false - }); - }); - } + // { + // let services_ctx = services.clone(); + // let config_exists_state = config_exists.clone(); + // let _ = use_future(|| async move { + // let cfg = services_ctx.config.get_server_config().await; + // config_exists_state.set(if let Some(app_cfg) = &cfg { + // app_cfg.config.ipcheck.is_some() + // } else { + // false + // }); + // }); + // } html! { Html { services_ctx_clone.toastr.error(msg); }, EventMessage::ConfigChange(config_type) => { - services_ctx_clone.toastr.warning_with_options(format!("{}: {config_type}", translate_clone.t("MESSAGES.CONFIG_CHANGED")), - ToastOptions { close_mode: ToastCloseMode::Manual }); + services_ctx_clone.toastr.warning_with_options( + format!("{}: {config_type}", translate_clone.t("MESSAGES.CONFIG_CHANGED")), + ToastOptions { close_mode: ToastCloseMode::Manual }); }, EventMessage::PlaylistUpdate(update_state) => { match update_state { diff --git a/frontend/src/app/components/loading_screen.rs b/frontend/src/app/components/loading_screen.rs new file mode 100644 index 000000000..fd363a4de --- /dev/null +++ b/frontend/src/app/components/loading_screen.rs @@ -0,0 +1,12 @@ +use yew::prelude::*; + +#[function_component] +pub fn LoadingScreen() -> Html { + html! { +
+ +
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/mod.rs b/frontend/src/app/components/mod.rs index d64e4c9a7..c70b7e0bc 100644 --- a/frontend/src/app/components/mod.rs +++ b/frontend/src/app/components/mod.rs @@ -42,6 +42,7 @@ mod role_based_content; mod api_user; mod cell_value; mod key_value_editor; +mod loading_screen; // pub use self::input::*; // pub use self::menu_item::*; // pub use self::popup_menu::*; @@ -81,5 +82,6 @@ pub use self::tabset::*; pub use self::role_based_content::*; pub use self::cell_value::*; pub use self::key_value_editor::*; +pub use self::loading_screen::*; //pub use self::number_input::*; //pub use self::date_input::*; \ No newline at end of file diff --git a/frontend/src/app/mod.rs b/frontend/src/app/mod.rs index 45873e680..d5dbdde56 100644 --- a/frontend/src/app/mod.rs +++ b/frontend/src/app/mod.rs @@ -13,7 +13,7 @@ use yew_i18n::I18nProvider; use yew::prelude::*; use yew_hooks::{use_async_with_options, UseAsyncOptions}; use yew_router::prelude::*; -use crate::app::components::{Authentication, Home, Login, RoleBasedContent}; +use crate::app::components::{Authentication, Home, LoadingScreen, Login, RoleBasedContent}; use crate::error::Error; use crate::hooks::{IconDefinition}; use crate::model::WebConfig; @@ -132,7 +132,7 @@ pub fn App() -> Html { if translations_state.as_ref().is_none() || configuration_state.as_ref().is_none() || icon_state.as_ref().is_none(){ - return html! {
{ "Loading..." }
}; + return html! {
}; } let transl = translations_state.as_ref().unwrap(); let config: &WebConfig = configuration_state.as_ref().unwrap(); diff --git a/frontend/src/hooks/use_service_context.rs b/frontend/src/hooks/use_service_context.rs index 17ffa6ea3..0ea6e633b 100644 --- a/frontend/src/hooks/use_service_context.rs +++ b/frontend/src/hooks/use_service_context.rs @@ -17,10 +17,10 @@ pub struct Services { impl Services { pub fn new(web_config: &WebConfig) -> Self { - let config = Rc::new(ConfigService::new(web_config)); + let event = Rc::new(EventService::new()); + let config = Rc::new(ConfigService::new(web_config, Rc::clone(&event))); let auth = Rc::new(AuthService::new()); let status = Rc::new(StatusService::new()); - let event = Rc::new(EventService::new()); let playlist = Rc::new(PlaylistService::new()); let toastr = Rc::new(ToastrService::new()); let user = Rc::new(UserService::new(Rc::clone(&event))); diff --git a/frontend/src/services/config_service.rs b/frontend/src/services/config_service.rs index b40c2cdb1..d40efcd6f 100644 --- a/frontend/src/services/config_service.rs +++ b/frontend/src/services/config_service.rs @@ -1,6 +1,6 @@ use crate::model::WebConfig; -use crate::services::{get_base_href, request_get}; -use shared::model::{AppConfigDto, ConfigInputDto, IpCheckDto}; +use crate::services::{get_base_href, request_get, request_post, EventService}; +use shared::model::{AppConfigDto, ConfigDto, ConfigInputDto, IpCheckDto}; use std::cell::RefCell; use std::future::Future; use std::rc::Rc; @@ -11,6 +11,7 @@ use futures_signals::signal::SignalExt; use shared::foundation::filter::{get_filter, prepare_templates}; use shared::foundation::mapper::MapperScript; use shared::utils::{concat_path, concat_path_leading_slash}; +use crate::error::Error; pub struct ConfigService { pub ui_config: Rc, @@ -20,10 +21,11 @@ pub struct ConfigService { config_path: String, ip_check_path: String, batch_input_content_path: String, + event_service: Rc } impl ConfigService { - pub fn new(config: &WebConfig) -> Self { + pub fn new(config: &WebConfig, event_service: Rc) -> Self { let base_href = get_base_href(); Self { ui_config: Rc::new(config.clone()), @@ -33,6 +35,7 @@ impl ConfigService { config_path: concat_path_leading_slash(&base_href, "api/v1/config"), ip_check_path: concat_path_leading_slash(&base_href, "api/v1/ipinfo"), batch_input_content_path: concat_path_leading_slash(&base_href, "api/v1/config/batchContent"), + event_service } } @@ -116,4 +119,21 @@ impl ConfigService { } } } + + pub async fn save_config(&self, dto: ConfigDto) -> Result<(), Error> { + let path = concat_path(&self.config_path, "main"); + self.event_service.set_config_change_message_blocked(true); + match request_post::(&path, dto, None, None).await { + Ok(()) => { + self.event_service.set_config_change_message_blocked(false); + Ok(()) + }, + Err(err) => { + self.event_service.set_config_change_message_blocked(false); + error!("{err}"); + Err(err) + } + } + } + } diff --git a/frontend/src/services/event_service.rs b/frontend/src/services/event_service.rs index 28a333514..64da8a9ae 100644 --- a/frontend/src/services/event_service.rs +++ b/frontend/src/services/event_service.rs @@ -9,7 +9,7 @@ type Subscriber = RefCell>>; pub struct EventService { subscriber_id: Rc, subscribers: Rc, - block_config_updated_message: AtomicBool, + block_config_updated_message: Rc, } impl Default for EventService { @@ -24,7 +24,7 @@ impl EventService { Self { subscriber_id: Rc::new(AtomicUsize::new(0)), subscribers: Rc::new(RefCell::new(HashMap::new())), - block_config_updated_message: AtomicBool::new(false), + block_config_updated_message: Rc::new(AtomicBool::new(false)), } } @@ -33,7 +33,14 @@ impl EventService { } pub fn set_config_change_message_blocked(&self, value: bool) { - self.block_config_updated_message.store(value, Ordering::Relaxed); + if value { + self.block_config_updated_message.store(value, Ordering::Relaxed); + } else { + let flag = Rc::clone(&self.block_config_updated_message); + let _ = gloo_timers::callback::Timeout::new(500, move || { + flag.store(value, Ordering::Relaxed); + }); + } } pub fn subscribe(&self, callback: F) -> usize { diff --git a/shared/src/model/config/base.rs b/shared/src/model/config/base.rs index 33c88e265..bd888adaf 100644 --- a/shared/src/model/config/base.rs +++ b/shared/src/model/config/base.rs @@ -121,6 +121,12 @@ pub struct SchedulesConfigDto { pub schedules: Option>, } +impl SchedulesConfigDto { + pub fn is_empty(&self) -> bool { + self.schedules.is_none() || self.schedules.as_ref().unwrap().is_empty() + } +} + impl From<&ConfigDto> for SchedulesConfigDto { fn from(config: &ConfigDto) -> Self { Self { @@ -223,4 +229,16 @@ impl ConfigDto { devices: hdhr.devices.iter().map(|d| d.name.to_string()).collect::>(), }) } + + pub fn update_from_main_config(&mut self, main_config: &MainConfigDto) { + self.threads = main_config.threads; + self.working_dir = main_config.working_dir.clone(); + self.backup_dir = main_config.backup_dir.clone(); + self.user_config_dir = main_config.user_config_dir.clone(); + self.mapping_path = main_config.mapping_path.clone(); + self.custom_stream_response_path = main_config.custom_stream_response_path.clone(); + self.user_access_control = main_config.user_access_control; + self.connect_timeout_secs = main_config.connect_timeout_secs; + self.sleep_timer_mins = main_config.sleep_timer_mins; + } } \ No newline at end of file diff --git a/shared/src/model/config/cache.rs b/shared/src/model/config/cache.rs index c9612a3f6..d80d9e0d4 100644 --- a/shared/src/model/config/cache.rs +++ b/shared/src/model/config/cache.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use crate::error::{TuliproxError}; use crate::info_err; -use crate::utils::parse_size_base_2; +use crate::utils::{is_blank_optional_string, parse_size_base_2}; use path_clean::PathClean; @@ -17,6 +17,10 @@ pub struct CacheConfigDto { } impl CacheConfigDto { + pub fn is_empty(&self) -> bool { + !self.enabled && is_blank_optional_string(&self.size) && is_blank_optional_string(&self.dir) + } + pub(crate) fn prepare(&mut self, working_dir: &str) -> Result<(), TuliproxError> { if self.enabled { let work_path = PathBuf::from(working_dir); diff --git a/shared/src/model/config/hdhomerun.rs b/shared/src/model/config/hdhomerun.rs index 74ed96fca..85f5a592f 100644 --- a/shared/src/model/config/hdhomerun.rs +++ b/shared/src/model/config/hdhomerun.rs @@ -89,6 +89,13 @@ pub struct HdHomeRunConfigDto { } impl HdHomeRunConfigDto { + pub fn is_empty(&self) -> bool { + !self.enabled && !self.auth && self.devices.is_empty() + } + + pub fn clean(&mut self) { + } + pub fn prepare(&mut self, api_port: u16) -> Result<(), TuliproxError> { let mut names = HashSet::new(); let mut ports = HashSet::new(); diff --git a/shared/src/model/config/ipcheck.rs b/shared/src/model/config/ipcheck.rs index 5a126b4b9..bc91afaea 100644 --- a/shared/src/model/config/ipcheck.rs +++ b/shared/src/model/config/ipcheck.rs @@ -1,5 +1,6 @@ -use regex::Regex; use crate::error::{TuliproxError, TuliproxErrorKind}; +use crate::utils::is_blank_optional_string; +use regex::Regex; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)] #[serde(deny_unknown_fields)] @@ -23,10 +24,35 @@ pub struct IpCheckConfigDto { /// Optional regex pattern to extract IPv6 #[serde(default, skip_serializing_if = "Option::is_none")] pub pattern_ipv6: Option, - } impl IpCheckConfigDto { + pub fn is_empty(&self) -> bool { + is_blank_optional_string(&self.url) + && is_blank_optional_string(&self.url_ipv4) + && is_blank_optional_string(&self.url_ipv6) + && is_blank_optional_string(&self.pattern_ipv4) + && is_blank_optional_string(&self.pattern_ipv6) + } + + pub fn clean(&mut self) { + if is_blank_optional_string(&self.url) { + self.url = None; + } + if is_blank_optional_string(&self.url_ipv4) { + self.url_ipv4 = None; + } + if is_blank_optional_string(&self.url_ipv6) { + self.url_ipv6 = None; + } + if is_blank_optional_string(&self.pattern_ipv4) { + self.pattern_ipv4 = None; + } + if is_blank_optional_string(&self.pattern_ipv6) { + self.pattern_ipv6 = None; + } + } + pub fn prepare(&mut self) -> Result<(), TuliproxError> { if let Some(url) = &self.url { let url = url.trim(); @@ -53,8 +79,11 @@ impl IpCheckConfigDto { } } - if self.url.is_none() && self.url_ipv4.is_none() && self.url_ipv6.is_none() { - return Err(TuliproxError::new(TuliproxErrorKind::Info, "No url provided!".to_owned())); + if self.url.is_none() && self.url_ipv4.is_none() && self.url_ipv6.is_none() { + return Err(TuliproxError::new( + TuliproxErrorKind::Info, + "No url provided!".to_owned(), + )); } // TODO allow or do not allow ? @@ -63,10 +92,20 @@ impl IpCheckConfigDto { // } if let Some(p4) = &self.pattern_ipv4 { - Regex::new(p4).map_err(|err| TuliproxError::new(TuliproxErrorKind::Info, format!("Invalid IPv4 regex: {p4} {err}")))?; + Regex::new(p4).map_err(|err| { + TuliproxError::new( + TuliproxErrorKind::Info, + format!("Invalid IPv4 regex: {p4} {err}"), + ) + })?; } if let Some(p6) = &self.pattern_ipv6 { - Regex::new(p6).map_err(|err| TuliproxError::new(TuliproxErrorKind::Info, format!("Invalid IPv6 regex: {p6} {err}")))?; + Regex::new(p6).map_err(|err| { + TuliproxError::new( + TuliproxErrorKind::Info, + format!("Invalid IPv6 regex: {p6} {err}"), + ) + })?; } Ok(()) } diff --git a/shared/src/model/config/log.rs b/shared/src/model/config/log.rs index ca17fdedb..998a08740 100644 --- a/shared/src/model/config/log.rs +++ b/shared/src/model/config/log.rs @@ -1,4 +1,4 @@ -use crate::utils::{default_as_true}; +use crate::utils::{default_as_true, is_blank_optional_string}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] @@ -19,4 +19,16 @@ impl Default for LogConfigDto { log_level: None, } } +} + +impl LogConfigDto { + pub fn is_empty(&self) -> bool { + self.sanitize_sensitive_info && !self.log_active_user && is_blank_optional_string(&self.log_level) + } + + pub fn clean(&mut self) { + if is_blank_optional_string(&self.log_level) { + self.log_level = None; + } + } } \ No newline at end of file diff --git a/shared/src/model/config/messaging.rs b/shared/src/model/config/messaging.rs index 5a6f93a27..c6f9ea33f 100644 --- a/shared/src/model/config/messaging.rs +++ b/shared/src/model/config/messaging.rs @@ -1,4 +1,5 @@ use crate::model::MsgKind; +use crate::utils::is_blank_optional_string; #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] @@ -7,12 +8,24 @@ pub struct TelegramMessagingConfigDto { pub chat_ids: Vec, } +impl TelegramMessagingConfigDto { + pub fn is_empty(&self) -> bool { + self.bot_token.trim().is_empty() || self.chat_ids.is_empty() + } +} + #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct RestMessagingConfigDto { pub url: String, } +impl RestMessagingConfigDto { + pub fn is_empty(&self) -> bool { + self.url.trim().is_empty() + } +} + #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct PushoverMessagingConfigDto { @@ -21,6 +34,14 @@ pub struct PushoverMessagingConfigDto { pub user: String, } +impl PushoverMessagingConfigDto { + pub fn is_empty(&self) -> bool { + is_blank_optional_string(&self.url) + && self.token.trim().is_empty() + && self.user.trim().is_empty() + } +} + #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct MessagingConfigDto { @@ -32,5 +53,26 @@ pub struct MessagingConfigDto { pub rest: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub pushover: Option, +} +impl MessagingConfigDto { + pub fn is_empty(&self) -> bool { + self.notify_on.is_empty() + && (self.telegram.is_none() || self.telegram.as_ref().is_some_and(|c| c.is_empty())) + && (self.rest.is_none() || self.rest.as_ref().is_some_and(|c| c.is_empty())) + && (self.pushover.is_none() || self.pushover.as_ref().is_some_and(|c| c.is_empty())) + } + + pub fn clean(&mut self) { + if self.telegram.as_ref().is_some_and(|c| c.is_empty()) { + self.telegram = None; + } + if self.rest.as_ref().is_some_and(|c| c.is_empty()) { + self.rest = None; + } + if self.pushover.as_ref().is_some_and(|c| c.is_empty()) { + self.pushover = None; + } + + } } \ No newline at end of file diff --git a/shared/src/model/config/proxy.rs b/shared/src/model/config/proxy.rs index b92037e51..c9c086005 100644 --- a/shared/src/model/config/proxy.rs +++ b/shared/src/model/config/proxy.rs @@ -1,4 +1,5 @@ use crate::error::{TuliproxError, TuliproxErrorKind}; +use crate::utils::is_blank_optional_string; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)] #[serde(deny_unknown_fields)] @@ -9,6 +10,21 @@ pub struct ProxyConfigDto { } impl ProxyConfigDto { + pub fn is_empty(&self) -> bool { + is_blank_optional_string(&self.username) + && is_blank_optional_string(&self.password) + && self.url.trim().is_empty() + } + + pub fn clean(&mut self) { + if is_blank_optional_string(&self.username) { + self.username = None; + } + if is_blank_optional_string(&self.password) { + self.password = None; + } + } + pub fn prepare(&mut self) -> Result<(), TuliproxError> { if self.username.is_some() || self.password.is_some() { if let (Some(username), Some(password)) = (self.username.as_ref(), self.password.as_ref()) { diff --git a/shared/src/model/config/rate_limit.rs b/shared/src/model/config/rate_limit.rs index ad2aecdf2..31a059e25 100644 --- a/shared/src/model/config/rate_limit.rs +++ b/shared/src/model/config/rate_limit.rs @@ -9,6 +9,10 @@ pub struct RateLimitConfigDto { } impl RateLimitConfigDto { + pub fn is_empty(&self) -> bool { + !self.enabled && self.period_millis == 0 && self.burst_size == 0 + } + pub(crate) fn prepare(&self) -> Result<(), TuliproxError> { if self.period_millis == 0 { return Err(TuliproxError::new(TuliproxErrorKind::Info, "Rate limiter period can't be 0".to_string())); diff --git a/shared/src/model/config/reverse_proxy.rs b/shared/src/model/config/reverse_proxy.rs index 529b357c1..15d893059 100644 --- a/shared/src/model/config/reverse_proxy.rs +++ b/shared/src/model/config/reverse_proxy.rs @@ -1,6 +1,6 @@ -use log::warn; use crate::error::TuliproxError; use crate::model::{CacheConfigDto, RateLimitConfigDto, StreamConfigDto}; +use log::warn; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)] #[serde(deny_unknown_fields)] @@ -18,6 +18,26 @@ pub struct ReverseProxyConfigDto { } impl ReverseProxyConfigDto { + pub fn is_empty(&self) -> bool { + !self.resource_rewrite_disabled + && self.disable_referer_header + && (self.stream.is_none() || self.stream.as_ref().is_some_and(|s| s.is_empty())) + && (self.cache.is_none() || self.cache.as_ref().is_some_and(|c| c.is_empty())) + && (self.rate_limit.is_none() || self.rate_limit.as_ref().is_some_and(|r| r.is_empty())) + } + + pub fn clean(&mut self) { + if self.stream.as_ref().is_some_and(|s| s.is_empty()) { + self.stream = None; + } + if self.cache.as_ref().is_some_and(|s| s.is_empty()) { + self.cache = None; + } + if self.rate_limit.as_ref().is_some_and(|s| s.is_empty()) { + self.rate_limit = None; + } + } + pub(crate) fn prepare(&mut self, working_dir: &str) -> Result<(), TuliproxError> { if let Some(stream) = self.stream.as_mut() { stream.prepare()?; diff --git a/shared/src/model/config/stream.rs b/shared/src/model/config/stream.rs index 2275bd18f..28c929dc0 100644 --- a/shared/src/model/config/stream.rs +++ b/shared/src/model/config/stream.rs @@ -15,6 +15,9 @@ pub struct StreamBufferConfigDto { impl StreamBufferConfigDto { + pub fn is_empty(&self) -> bool { + !self.enabled && self.size == 0 + } fn prepare(&mut self) { if self.enabled && self.size == 0 { self.size = STREAM_QUEUE_SIZE; @@ -56,6 +59,18 @@ impl Default for StreamConfigDto { } impl StreamConfigDto { + pub fn is_empty(&self) -> bool { + let empty = Self::default(); + self.retry == empty.retry + && (self.buffer.is_none() || self.buffer.as_ref().is_some_and(|b| b.is_empty())) + && (self.throttle.is_none() || self.throttle.as_ref().is_some_and(|t| t.is_empty())) + && self.grace_period_millis == empty.grace_period_millis + && self.grace_period_timeout_secs == empty.grace_period_timeout_secs + && self.forced_retry_interval_secs == empty.forced_retry_interval_secs + && self.throttle_kbps == empty.throttle_kbps + } + + pub(crate) fn prepare(&mut self) -> Result<(), TuliproxError> { if let Some(buffer) = self.buffer.as_mut() { buffer.prepare(); diff --git a/shared/src/model/config/video_download.rs b/shared/src/model/config/video_download.rs index 2353211f3..dfcab878b 100644 --- a/shared/src/model/config/video_download.rs +++ b/shared/src/model/config/video_download.rs @@ -3,6 +3,7 @@ use std::borrow::BorrowMut; use crate::create_tuliprox_error_result; use crate::error::{TuliproxError, TuliproxErrorKind}; use crate::model::DEFAULT_USER_AGENT; +use crate::utils::is_blank_optional_string; pub const DEFAULT_VIDEO_EXTENSIONS: [&str; 6] = ["mkv", "avi", "mp4", "mpeg", "divx", "mov"]; @@ -19,6 +20,15 @@ pub struct VideoDownloadConfigDto { pub episode_pattern: Option, } +impl VideoDownloadConfigDto { + pub fn is_empty(&self) -> bool { + !self.organize_into_directories + && self.headers.is_empty() + && is_blank_optional_string(&self.directory) + && is_blank_optional_string(&self.episode_pattern) + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)] #[serde(deny_unknown_fields)] pub struct VideoConfigDto { @@ -32,6 +42,17 @@ pub struct VideoConfigDto { impl VideoConfigDto { + pub fn is_empty(&self) -> bool { + self.extensions.is_empty() && is_blank_optional_string(&self.web_search) + && (self.download.is_none() || self.download.as_ref().is_some_and(|d| d.is_empty())) + } + + pub fn clean(&mut self) { + if self.download.as_ref().is_some_and(|d| d.is_empty()) { + self.download = None; + } + } + /// # Panics /// /// Will panic if default `RegEx` gets invalid diff --git a/shared/src/model/config/web_auth.rs b/shared/src/model/config/web_auth.rs index 912fa90c8..3148f4965 100644 --- a/shared/src/model/config/web_auth.rs +++ b/shared/src/model/config/web_auth.rs @@ -1,4 +1,4 @@ -use crate::utils::{default_as_true}; +use crate::utils::{default_as_true, is_blank_optional_string}; /// 30 minutes by default; `0` still means “no expiration.” fn default_token_ttl_mins() -> u32 { 30 @@ -27,4 +27,15 @@ impl Default for WebAuthConfigDto { userfile: None, } } -} \ No newline at end of file +} + +impl WebAuthConfigDto { + pub fn is_empty(&self) -> bool { + let empty = WebAuthConfigDto::default(); + self.enabled == empty.enabled + && self.token_ttl_mins == empty.token_ttl_mins + && self.issuer.trim().is_empty() + && self.secret.trim().is_empty() + && is_blank_optional_string(&self.userfile) + } +} diff --git a/shared/src/model/config/web_ui.rs b/shared/src/model/config/web_ui.rs index 838682f91..c0aac7aeb 100644 --- a/shared/src/model/config/web_ui.rs +++ b/shared/src/model/config/web_ui.rs @@ -1,6 +1,6 @@ use crate::error::{TuliproxError, TuliproxErrorKind}; use crate::model::WebAuthConfigDto; -use crate::utils::default_as_true; +use crate::utils::{default_as_true, is_blank_optional_string}; const RESERVED_PATHS: &[&str] = &[ "live", @@ -31,6 +31,15 @@ pub struct ContentSecurityPolicyConfigDto { } impl ContentSecurityPolicyConfigDto { + pub fn is_empty(&self) -> bool { + !self.enabled + && (self.custom_attributes.is_none() + || self + .custom_attributes + .as_ref() + .is_some_and(|v| v.is_empty())) + } + pub fn validate(&self) -> Result<(), TuliproxError> { if let Some(attrs) = self.custom_attributes.as_ref() { for (i, attr) in attrs.iter().enumerate() { @@ -57,7 +66,6 @@ impl ContentSecurityPolicyConfigDto { } } - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct WebUiConfigDto { @@ -89,6 +97,40 @@ impl Default for WebUiConfigDto { } impl WebUiConfigDto { + pub fn is_empty(&self) -> bool { + let empty = WebUiConfigDto::default(); + self.enabled == empty.enabled + && self.user_ui_enabled == empty.user_ui_enabled + && is_blank_optional_string(&self.path) + && is_blank_optional_string(&self.player_server) + && (self.content_security_policy.is_none() + || self + .content_security_policy + .as_ref() + .is_some_and(|c| c.is_empty())) + && (self.auth.is_none() || self.auth.as_ref().is_some_and(|c| c.is_empty())) + } + + pub fn clean(&mut self) { + if self + .content_security_policy + .as_ref() + .is_some_and(|c| c.is_empty()) + { + self.content_security_policy = None; + } + if self.auth.as_ref().is_some_and(|c| c.is_empty()) { + self.auth = None; + } + + if is_blank_optional_string(&self.path) { + self.path = None; + } + if is_blank_optional_string(&self.player_server) { + self.player_server = None; + } + } + pub fn prepare(&mut self) -> Result<(), TuliproxError> { if !self.enabled { self.auth = None; @@ -118,4 +160,4 @@ impl WebUiConfigDto { } Ok(()) } -} \ No newline at end of file +} diff --git a/shared/src/utils/string_utils.rs b/shared/src/utils/string_utils.rs index 5b1fb9351..34a706c48 100644 --- a/shared/src/utils/string_utils.rs +++ b/shared/src/utils/string_utils.rs @@ -58,6 +58,10 @@ pub fn get_non_empty_str<'a>(first: &'a str, second: &'a str, third: &'a str) -> } } +pub fn is_blank_optional_string(s: &Option) -> bool { + s.is_none() || s.as_ref().is_some_and(|s| s.trim().is_empty()) +} + pub fn trim_slash(s: &str) -> Cow<'_, str> { let trimmed = s.trim_matches('/'); if trimmed.len() == s.len() {