save config, show modified pages

This commit is contained in:
euzu
2025-08-31 15:48:27 +02:00
parent 5a37a1203d
commit a5b350c6a5
12 changed files with 220 additions and 192 deletions
@@ -34,9 +34,9 @@ pub fn ApiConfigView() -> Html {
{
let on_form_change = config_view_ctx.on_form_change.clone();
let deps = (form_state.form.clone(), form_state.modified);
use_effect_with(deps, move |(form, modified)| {
on_form_change.emit(ConfigForm::Api(*modified, form.clone()));
let deps = (form_state.clone(), form_state.modified);
use_effect_with(deps, move |(state, modified)| {
on_form_change.emit(ConfigForm::Api(*modified, state.form.clone()));
|| ()
});
}
@@ -70,12 +70,12 @@ pub fn ConfigView() -> Html {
let translate = translate.clone();
let edit_mode = edit_mode.clone();
use_memo((form_state, edit_mode), move |(forms, edit)| {
let modified = collect_modified!(forms, [
main, api, schedules, video, messaging, web_ui,
reverse_proxy, hd_homerun, proxy, ipcheck
])
.iter()
use_memo((form_state, edit_mode, translate), move |(forms, edit, translate)| {
let forms: &ConfigFormState = forms;
let modified_pages = collect_modified!(forms, [
main, api, 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))
.collect::<Vec<ConfigPage>>();
@@ -94,7 +94,7 @@ pub fn ConfigView() -> Html {
let editing = **edit;
tab_configs.into_iter().map(|(page, label, children, icon)| {
let is_modified = editing && modified.contains(&page);
let is_modified = editing && modified_pages.contains(&page);
TabItem {
id: page.to_string(),
title: translate.t(label),
@@ -31,8 +31,9 @@ pub fn HdHomerunConfigView() -> Html {
{
let on_form_change = config_view_ctx.on_form_change.clone();
use_effect_with(form_state.clone(), move |state| {
on_form_change.emit(ConfigForm::HdHomerun(state.modified, state.form.clone()));
let deps = (form_state.clone(), form_state.modified);
use_effect_with(deps, move |(state, modified)| {
on_form_change.emit(ConfigForm::HdHomerun(*modified, state.form.clone()));
});
}
@@ -37,8 +37,9 @@ pub fn IpCheckConfigView() -> Html {
{
let on_form_change = config_view_ctx.on_form_change.clone();
use_effect_with(form_state.clone(), move |state| {
on_form_change.emit(ConfigForm::IpCheck(state.modified, state.form.clone()));
let deps = (form_state.clone(), form_state.modified);
use_effect_with(deps, move |(state, modified)| {
on_form_change.emit(ConfigForm::IpCheck(*modified, state.form.clone()));
});
}
@@ -49,8 +49,9 @@ pub fn MainConfigView() -> Html {
{
let on_form_change = config_view_ctx.on_form_change.clone();
use_effect_with(form_state.clone(), move |state| {
on_form_change.emit(ConfigForm::Main(state.modified, state.form.clone()));
let deps = (form_state.clone(), form_state.modified);
use_effect_with(deps, move |(state, modified)| {
on_form_change.emit(ConfigForm::Main(*modified, state.form.clone()));
});
}
@@ -1,15 +1,20 @@
use std::rc::Rc;
use std::str::FromStr;
use crate::app::components::{Card, Chip, RadioButtonGroup};
use crate::app::context::ConfigContext;
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 yew::prelude::*;
use yew_i18n::use_translation;
use crate::app::components::config::config_page::ConfigForm;
use crate::app::components::config::config_view_context::ConfigViewContext;
use crate::app::components::config::macros::HasFormData;
use crate::app::components::{Card, Chip, RadioButtonGroup};
use crate::app::context::ConfigContext;
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 std::rc::Rc;
use std::str::FromStr;
use yew::prelude::*;
use yew_i18n::use_translation;
const LABEL_NOTIFY_ON: &str = "LABEL.NOTIFY_ON";
const LABEL_TELEGRAM: &str = "LABEL.TELEGRAM";
@@ -56,34 +61,43 @@ generate_form_reducer!(
}
);
#[function_component]
pub fn MessagingConfigView() -> Html {
let translate = use_translation();
let config_ctx = use_context::<ConfigContext>().expect("ConfigContext not found");
let config_view_ctx = use_context::<ConfigViewContext>().expect("ConfigViewContext not found");
let telegram_state: UseReducerHandle<TelegramMessagingConfigFormState> = use_reducer(|| {
TelegramMessagingConfigFormState { form: TelegramMessagingConfigDto::default(), modified: false }
});
let rest_state: UseReducerHandle<RestMessagingConfigFormState> = use_reducer(|| {
RestMessagingConfigFormState { form: RestMessagingConfigDto::default(), modified: false }
});
let telegram_state: UseReducerHandle<TelegramMessagingConfigFormState> =
use_reducer(|| TelegramMessagingConfigFormState {
form: TelegramMessagingConfigDto::default(),
modified: false,
});
let rest_state: UseReducerHandle<RestMessagingConfigFormState> =
use_reducer(|| RestMessagingConfigFormState {
form: RestMessagingConfigDto::default(),
modified: false,
});
let pushover_state: UseReducerHandle<PushoverMessagingConfigFormState> = use_reducer(|| {
PushoverMessagingConfigFormState { form: PushoverMessagingConfigDto::default(), modified: false }
});
let pushover_state: UseReducerHandle<PushoverMessagingConfigFormState> =
use_reducer(|| PushoverMessagingConfigFormState {
form: PushoverMessagingConfigDto::default(),
modified: false,
});
let messaging_state: UseReducerHandle<MessagingConfigFormState> = use_reducer(|| {
MessagingConfigFormState { form: MessagingConfigDto::default(), modified: false }
});
let messaging_state: UseReducerHandle<MessagingConfigFormState> =
use_reducer(|| MessagingConfigFormState {
form: MessagingConfigDto::default(),
modified: false,
});
let notify_on_options = use_memo((), |_| vec![
MsgKind::Info.to_string(),
MsgKind::Stats.to_string(),
MsgKind::Error.to_string(),
MsgKind::Watch.to_string(),
]);
let notify_on_options = use_memo((), |_| {
vec![
MsgKind::Info.to_string(),
MsgKind::Stats.to_string(),
MsgKind::Error.to_string(),
MsgKind::Watch.to_string(),
]
});
{
let on_form_change = config_view_ctx.on_form_change.clone();
@@ -92,87 +106,88 @@ pub fn MessagingConfigView() -> Html {
let rest_state = rest_state.clone();
let pushover_state = pushover_state.clone();
use_effect_with(
(messaging_state, telegram_state, rest_state, pushover_state),
move |(m, t, r, p)| {
let mut form = m.form.clone();
form.telegram = Some(t.form.clone());
form.rest = Some(r.form.clone());
form.pushover = Some(p.form.clone());
let modified = m.modified || t.modified || r.modified || p.modified;
on_form_change.emit(ConfigForm::Messaging(modified, form));
},
let deps = (
messaging_state.modified,
telegram_state.modified,
rest_state.modified,
pushover_state.modified,
messaging_state,
telegram_state,
rest_state,
pushover_state,
);
use_effect_with(deps, move |(mm, tm, rm, pm, m, t, r, p)| {
let mut form = m.form.clone();
form.telegram = Some(t.form.clone());
form.rest = Some(r.form.clone());
form.pushover = Some(p.form.clone());
let modified = *mm || *tm || *rm || *pm;
on_form_change.emit(ConfigForm::Messaging(modified, form));
});
}
let render_telegram = |telegram: Option<&TelegramMessagingConfigDto>| {
match telegram {
Some(entry) => html! {
<Card class="tp__config-view__card">
<h1>{translate.t("LABEL.TELEGRAM")}</h1>
{ config_field!(entry, translate.t(LABEL_BOT_TOKEN), bot_token) }
{ config_field_child!(translate.t(LABEL_CHAT_IDS), {
html! {
<div class="tp__config-view__tags">
{
if entry.chat_ids.is_empty() {
html! {}
} else {
html! { for entry.chat_ids.iter().map(|t| html! { <Chip label={t.clone()} /> }) }
}
}
</div>
}
})}
</Card>
},
None => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_TELEGRAM)}</h1>
{ config_field_empty!(translate.t(LABEL_BOT_TOKEN)) }
{ config_field_empty!(translate.t(LABEL_CHAT_IDS)) }
</Card>
},
}
let render_telegram = |telegram: Option<&TelegramMessagingConfigDto>| match telegram {
Some(entry) => html! {
<Card class="tp__config-view__card">
<h1>{translate.t("LABEL.TELEGRAM")}</h1>
{ config_field!(entry, translate.t(LABEL_BOT_TOKEN), bot_token) }
{ config_field_child!(translate.t(LABEL_CHAT_IDS), {
html! {
<div class="tp__config-view__tags">
{
if entry.chat_ids.is_empty() {
html! {}
} else {
html! { for entry.chat_ids.iter().map(|t| html! { <Chip label={t.clone()} /> }) }
}
}
</div>
}
})}
</Card>
},
None => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_TELEGRAM)}</h1>
{ config_field_empty!(translate.t(LABEL_BOT_TOKEN)) }
{ config_field_empty!(translate.t(LABEL_CHAT_IDS)) }
</Card>
},
};
let render_rest = |rest: Option<&RestMessagingConfigDto>| {
match rest {
Some(entry) => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_REST)}</h1>
{ config_field!(entry, translate.t(LABEL_URL), url) }
</Card>
},
None => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_REST)}</h1>
{ config_field_empty!(translate.t(LABEL_URL)) }
</Card>
},
}
let render_rest = |rest: Option<&RestMessagingConfigDto>| match rest {
Some(entry) => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_REST)}</h1>
{ config_field!(entry, translate.t(LABEL_URL), url) }
</Card>
},
None => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_REST)}</h1>
{ config_field_empty!(translate.t(LABEL_URL)) }
</Card>
},
};
let render_pushover = |pushover: Option<&PushoverMessagingConfigDto>| {
match pushover {
Some(entry) => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_PUSHOVER)}</h1>
{ config_field_optional!(entry, translate.t(LABEL_URL), url) }
{ config_field_hide!(entry, translate.t(LABEL_TOKEN), token) }
{ config_field!(entry, translate.t(LABEL_USER), user) }
</Card>
},
None => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_PUSHOVER)}</h1>
{ config_field_empty!(translate.t(LABEL_URL)) }
{ config_field_empty!(translate.t(LABEL_TOKEN)) }
{ config_field_empty!(translate.t(LABEL_USER)) }
</Card>
},
}
let render_pushover = |pushover: Option<&PushoverMessagingConfigDto>| match pushover {
Some(entry) => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_PUSHOVER)}</h1>
{ config_field_optional!(entry, translate.t(LABEL_URL), url) }
{ config_field_hide!(entry, translate.t(LABEL_TOKEN), token) }
{ config_field!(entry, translate.t(LABEL_USER), user) }
</Card>
},
None => html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_PUSHOVER)}</h1>
{ config_field_empty!(translate.t(LABEL_URL)) }
{ config_field_empty!(translate.t(LABEL_TOKEN)) }
{ config_field_empty!(translate.t(LABEL_USER)) }
</Card>
},
};
let render_empty = || {
@@ -194,21 +209,21 @@ pub fn MessagingConfigView() -> Html {
if let Some(config) = &config_ctx.config {
if let Some(messaging) = &config.config.messaging {
html! {
<>
<div class="tp__messaging-config-view__header tp__config-view-page__header">
{ config_field_child!(translate.t(LABEL_NOTIFY_ON), {
html! { <div class="tp__messaging-config-view__notify-on">
{ for messaging.notify_on.iter().map(|t| html! { <Chip label={t.to_string()} /> }) }
</div> }
})}
</div>
<div class="tp__messaging-config-view__body tp__config-view-page__body">
{render_telegram(messaging.telegram.as_ref())}
{render_rest(messaging.rest.as_ref())}
{render_pushover(messaging.pushover.as_ref())}
</div>
</>
}
<>
<div class="tp__messaging-config-view__header tp__config-view-page__header">
{ config_field_child!(translate.t(LABEL_NOTIFY_ON), {
html! { <div class="tp__messaging-config-view__notify-on">
{ for messaging.notify_on.iter().map(|t| html! { <Chip label={t.to_string()} /> }) }
</div> }
})}
</div>
<div class="tp__messaging-config-view__body tp__config-view-page__body">
{render_telegram(messaging.telegram.as_ref())}
{render_rest(messaging.rest.as_ref())}
{render_pushover(messaging.pushover.as_ref())}
</div>
</>
}
} else {
render_empty()
}
@@ -219,47 +234,55 @@ pub fn MessagingConfigView() -> Html {
let render_edit_mode = || {
let msg_state = messaging_state.clone();
let notify_on_selections = Rc::new(msg_state.form.notify_on.iter().map(ToString::to_string).collect());
let notify_on_selections = Rc::new(
msg_state
.form
.notify_on
.iter()
.map(ToString::to_string)
.collect(),
);
html! {
<>
<div class="tp__messaging-config-view__header tp__config-view-page__header">
{ config_field_child!(translate.t("LABEL.NOTIFY_ON"), {
html! { <RadioButtonGroup
multi_select={true} none_allowed={true}
on_select={Callback::from(move |selections: Rc<Vec<String>>| {
msg_state.dispatch(MessagingConfigFormAction::NotifyOn(
selections.iter().filter_map(|s| MsgKind::from_str(s).ok()).collect()));
})}
options={notify_on_options.clone()}
selected={notify_on_selections}
/>
}})}
</div>
<div class="tp__messaging-config-view__body tp__config-view-page__body">
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_TELEGRAM)}</h1>
{ edit_field_text!(telegram_state, translate.t(LABEL_BOT_TOKEN), bot_token, TelegramMessagingConfigFormAction::BotToken) }
{ edit_field_list!(telegram_state, translate.t(LABEL_CHAT_IDS), chat_ids, TelegramMessagingConfigFormAction::ChatIds, translate.t("LABEL.ADD_CHAT_ID")) }
</Card>
<>
<div class="tp__messaging-config-view__header tp__config-view-page__header">
{ config_field_child!(translate.t("LABEL.NOTIFY_ON"), {
html! { <RadioButtonGroup
multi_select={true} none_allowed={true}
on_select={Callback::from(move |selections: Rc<Vec<String>>| {
msg_state.dispatch(MessagingConfigFormAction::NotifyOn(
selections.iter().filter_map(|s| MsgKind::from_str(s).ok()).collect()));
})}
options={notify_on_options.clone()}
selected={notify_on_selections}
/>
}})}
</div>
<div class="tp__messaging-config-view__body tp__config-view-page__body">
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_TELEGRAM)}</h1>
{ edit_field_text!(telegram_state, translate.t(LABEL_BOT_TOKEN), bot_token, TelegramMessagingConfigFormAction::BotToken) }
{ edit_field_list!(telegram_state, translate.t(LABEL_CHAT_IDS), chat_ids, TelegramMessagingConfigFormAction::ChatIds, translate.t("LABEL.ADD_CHAT_ID")) }
</Card>
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_REST)}</h1>
{ edit_field_text!(rest_state, translate.t(LABEL_URL), url, RestMessagingConfigFormAction::Url) }
</Card>
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_REST)}</h1>
{ edit_field_text!(rest_state, translate.t(LABEL_URL), url, RestMessagingConfigFormAction::Url) }
</Card>
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_PUSHOVER)}</h1>
{ edit_field_text_option!(pushover_state, translate.t(LABEL_URL), url, PushoverMessagingConfigFormAction::Url) }
{ edit_field_text!(pushover_state, translate.t(LABEL_TOKEN), token, PushoverMessagingConfigFormAction::Token, true) }
{ edit_field_text!(pushover_state, translate.t(LABEL_USER), user, PushoverMessagingConfigFormAction::User) }
</Card>
</div>
</>
}};
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_PUSHOVER)}</h1>
{ edit_field_text_option!(pushover_state, translate.t(LABEL_URL), url, PushoverMessagingConfigFormAction::Url) }
{ edit_field_text!(pushover_state, translate.t(LABEL_TOKEN), token, PushoverMessagingConfigFormAction::Token, true) }
{ edit_field_text!(pushover_state, translate.t(LABEL_USER), user, PushoverMessagingConfigFormAction::User) }
</Card>
</div>
</>
}
};
html! {
<div class="tp__messaging-config-view tp__config-view-page">
{ if *config_view_ctx.edit_mode { render_edit_mode() } else { render_view_mode() } }
</div>
}
}
}
@@ -36,8 +36,9 @@ pub fn ProxyConfigView() -> Html {
{
let on_form_change = config_view_ctx.on_form_change.clone();
use_effect_with(form_state.clone(), move |state| {
on_form_change.emit(ConfigForm::Proxy(state.modified, state.form.clone()));
let deps = (form_state.clone(), form_state.modified);
use_effect_with(deps, move |(state, modified)| {
on_form_change.emit(ConfigForm::Proxy(*modified, state.form.clone()));
});
}
@@ -123,23 +123,9 @@ pub fn ReverseProxyConfigView() -> Html {
use_effect_with((reverse_proxy_cfg, config_view_ctx.edit_mode.clone()), move |(cfg, _mode)| {
if let Some(rp) = cfg {
reverse_proxy_state.dispatch(ReverseProxyConfigFormAction::SetAll((*rp).clone()));
if let Some(cache) = &rp.cache {
cache_state.dispatch(CacheConfigFormAction::SetAll(cache.clone()));
} else {
cache_state.dispatch(CacheConfigFormAction::SetAll(CacheConfigDto::default()));
}
if let Some(rl) = &rp.rate_limit {
rate_limit_state.dispatch(RateLimitConfigFormAction::SetAll(rl.clone()));
} else {
rate_limit_state.dispatch(RateLimitConfigFormAction::SetAll(RateLimitConfigDto::default()));
}
if let Some(stream) = &rp.stream {
stream_state.dispatch(StreamConfigFormAction::SetAll(stream.clone()));
} else {
stream_state.dispatch(StreamConfigFormAction::SetAll(StreamConfigDto::default()));
}
cache_state.dispatch(CacheConfigFormAction::SetAll(rp.cache.as_ref().map_or_else(CacheConfigDto::default, |c| c.clone())));
rate_limit_state.dispatch(RateLimitConfigFormAction::SetAll(rp.rate_limit.as_ref().map_or_else(RateLimitConfigDto::default, |rl| rl.clone())));
stream_state.dispatch(StreamConfigFormAction::SetAll(rp.stream.as_ref().map_or_else(StreamConfigDto::default, |s| s.clone())));
} else {
reverse_proxy_state.dispatch(ReverseProxyConfigFormAction::SetAll(ReverseProxyConfigDto::default()));
cache_state.dispatch(CacheConfigFormAction::SetAll(CacheConfigDto::default()));
@@ -61,8 +61,9 @@ pub fn SchedulesConfigView() -> Html {
{
let on_form_change = config_view_ctx.on_form_change.clone();
use_effect_with(form_state.clone(), move |state| {
on_form_change.emit(ConfigForm::Schedules(state.modified, state.form.clone()));
let deps = (form_state.clone(), form_state.modified);
use_effect_with(deps, move |(state, modified)| {
on_form_change.emit(ConfigForm::Schedules(*modified, state.form.clone()));
});
}
@@ -79,7 +79,8 @@ pub fn VideoConfigView() -> Html {
let on_form_change = config_view_ctx.on_form_change.clone();
let download_state = download_state.clone();
let video_state = video_state.clone();
use_effect_with((video_state.modified, download_state.modified, video_state, download_state),
let deps = (video_state.modified, download_state.modified, video_state, download_state);
use_effect_with(deps,
move |(vm, dm, v, d)| {
let mut form = v.form.clone();
form.download = if *dm { Some(d.form.clone()) } else { form.download };
@@ -79,8 +79,8 @@ pub fn WebUiConfigView() -> Html {
let webui_state = webui_state.clone();
let auth_state = auth_state.clone();
let csp_state = csp_state.clone();
use_effect_with((webui_state.modified, auth_state.modified, csp_state.modified, webui_state, auth_state, csp_state),
let deps = (webui_state.modified, auth_state.modified, csp_state.modified, webui_state, auth_state, csp_state);
use_effect_with(deps,
move |(wm, am, cm, w, a, c)| {
let mut form = w.form.clone();
form.auth = Some(a.form.clone());
+13
View File
@@ -12,6 +12,19 @@ pub struct TabItem {
pub inactive_class: Option<String>,
}
// impl TabItem {
// pub fn new(id: String, title: String, icon: String, children: Html) -> Self {
// Self {
// id,
// title,
// icon,
// children,
// active_class: None,
// inactive_class: None,
// }
// }
// }
#[derive(Properties, Clone, PartialEq)]
pub struct TabSetProps {
pub tabs: Rc<Vec<TabItem>>,