mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-24 16:52:28 +02:00
Refactor forms to use separate item forms with Cancel/Submit pattern
Create separate item form components: - Created epg_source_item_form.rs for EPG sources - Created alias_item_form.rs for input aliases - Created trakt_list_item_form.rs for Trakt lists Refactor parent forms: - input_form.rs now uses EpgSourceItemForm and AliasItemForm - output_xtream_form.rs now uses TraktListItemForm - Add-buttons toggle form visibility instead of inline inputs - Each item form has Cancel/Submit buttons - Removed inline form reducers, moved to item forms UI improvements: - Consistent form pattern across all list items - Better UX with explicit Cancel/Submit workflow - Cleaner code without dyn_into DOM manipulation i18n updates: - Added EPG_SOURCE_URL, EPG_SOURCES, ADD_EPG_SOURCE labels - Added ALIAS_NAME, ADD_ALIAS labels - Added all Trakt labels (TRAKT_USER, TRAKT_LIST_SLUG, etc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -245,6 +245,16 @@
|
||||
"DEVICE_TYPE": "Device Type",
|
||||
"DEVICE_UDN": "Device UDN",
|
||||
"TUNER_COUNT": "Tuner Count",
|
||||
"TRAKT_USER": "Trakt User",
|
||||
"TRAKT_LIST_SLUG": "Trakt List Slug",
|
||||
"TRAKT_CATEGORY_NAME": "Trakt Category Name",
|
||||
"TRAKT_CONTENT_TYPE": "Trakt Content Type",
|
||||
"TRAKT_FUZZY_MATCH_THRESHOLD": "Trakt Fuzzy Match Threshold (%)",
|
||||
"TRAKT_API_KEY": "Trakt API Key",
|
||||
"TRAKT_API_VERSION": "Trakt API Version",
|
||||
"TRAKT_API_URL": "Trakt API URL",
|
||||
"TRAKT_LISTS": "Trakt Lists",
|
||||
"ADD_TRAKT_LIST": "Add Trakt List",
|
||||
"SOURCE": "Source",
|
||||
"PRIORITY": "Priority",
|
||||
"LOGO_OVERRIDE": "logo Override",
|
||||
@@ -274,8 +284,13 @@
|
||||
"WATCH": "Watch",
|
||||
"INPUT_TYPE": "Input Type",
|
||||
"EPG": "Epg",
|
||||
"EPG_SOURCE_URL": "EPG Source URL",
|
||||
"EPG_SOURCES": "EPG Sources",
|
||||
"ADD_EPG_SOURCE": "Add EPG Source",
|
||||
"PERSIST": "Persist",
|
||||
"ALIASES": "Aliases",
|
||||
"ALIAS_NAME": "Alias Name",
|
||||
"ADD_ALIAS": "Add Alias",
|
||||
"MAX_CON": "Max Con.",
|
||||
"MAX_CONNECTIONS": "Max Connections",
|
||||
"METHOD": "Method",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
use crate::app::components::{Card, TextButton};
|
||||
use crate::{edit_field_date, edit_field_number_i16, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer};
|
||||
use shared::model::ConfigInputAliasDto;
|
||||
use yew::{function_component, html, use_reducer, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_ALIAS_NAME: &str = "LABEL.ALIAS_NAME";
|
||||
const LABEL_URL: &str = "LABEL.URL";
|
||||
const LABEL_USERNAME: &str = "LABEL.USERNAME";
|
||||
const LABEL_PASSWORD: &str = "LABEL.PASSWORD";
|
||||
const LABEL_PRIORITY: &str = "LABEL.PRIORITY";
|
||||
const LABEL_MAX_CONNECTIONS: &str = "LABEL.MAX_CONNECTIONS";
|
||||
const LABEL_EXP_DATE: &str = "LABEL.EXP_DATE";
|
||||
|
||||
generate_form_reducer!(
|
||||
state: AliasFormState { form: ConfigInputAliasDto },
|
||||
action_name: AliasFormAction,
|
||||
fields {
|
||||
Id => id: u16,
|
||||
Name => name: String,
|
||||
Url => url: String,
|
||||
Username => username: Option<String>,
|
||||
Password => password: Option<String>,
|
||||
Priority => priority: i16,
|
||||
MaxConnections => max_connections: u16,
|
||||
ExpDate => exp_date: Option<i64>,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct AliasItemFormProps {
|
||||
pub on_submit: Callback<ConfigInputAliasDto>,
|
||||
pub on_cancel: Callback<()>,
|
||||
#[prop_or_default]
|
||||
pub initial: Option<ConfigInputAliasDto>,
|
||||
#[prop_or(0)]
|
||||
pub next_id: u16,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn AliasItemForm(props: &AliasItemFormProps) -> Html {
|
||||
let translate = use_translation();
|
||||
|
||||
let form_state: UseReducerHandle<AliasFormState> = use_reducer(|| {
|
||||
AliasFormState {
|
||||
form: props.initial.clone().unwrap_or_else(|| ConfigInputAliasDto {
|
||||
id: props.next_id,
|
||||
name: String::new(),
|
||||
url: String::new(),
|
||||
username: None,
|
||||
password: None,
|
||||
priority: 0,
|
||||
max_connections: 1,
|
||||
exp_date: None,
|
||||
}),
|
||||
modified: false,
|
||||
}
|
||||
});
|
||||
|
||||
let handle_submit = {
|
||||
let form_state = form_state.clone();
|
||||
let on_submit = props.on_submit.clone();
|
||||
Callback::from(move |_| {
|
||||
let data = form_state.data().clone();
|
||||
if !data.name.trim().is_empty() && !data.url.trim().is_empty() {
|
||||
on_submit.emit(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let handle_cancel = {
|
||||
let on_cancel = props.on_cancel.clone();
|
||||
Callback::from(move |_| {
|
||||
on_cancel.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card tp__item-form">
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_ALIAS_NAME), name, AliasFormAction::Name) }
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_URL), url, AliasFormAction::Url) }
|
||||
{ edit_field_text_option!(form_state, translate.t(LABEL_USERNAME), username, AliasFormAction::Username) }
|
||||
{ edit_field_text_option!(form_state, translate.t(LABEL_PASSWORD), password, AliasFormAction::Password, true) }
|
||||
{ edit_field_number_i16!(form_state, translate.t(LABEL_PRIORITY), priority, AliasFormAction::Priority) }
|
||||
{ edit_field_number_u16!(form_state, translate.t(LABEL_MAX_CONNECTIONS), max_connections, AliasFormAction::MaxConnections) }
|
||||
{ edit_field_date!(form_state, translate.t(LABEL_EXP_DATE), exp_date, AliasFormAction::ExpDate) }
|
||||
|
||||
<div class="tp__item-form__buttons">
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="submit_alias"
|
||||
icon="Accept"
|
||||
title={translate.t("LABEL.SUBMIT")}
|
||||
onclick={handle_submit}
|
||||
/>
|
||||
<TextButton
|
||||
class="secondary"
|
||||
name="cancel_alias"
|
||||
icon="Cancel"
|
||||
title={translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use crate::app::components::{Card, TextButton};
|
||||
use crate::{edit_field_text, generate_form_reducer};
|
||||
use shared::model::EpgSourceDto;
|
||||
use yew::{function_component, html, use_reducer, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_EPG_SOURCE_URL: &str = "LABEL.EPG_SOURCE_URL";
|
||||
|
||||
generate_form_reducer!(
|
||||
state: EpgSourceFormState { form: EpgSourceDto },
|
||||
action_name: EpgSourceFormAction,
|
||||
fields {
|
||||
Url => url: String,
|
||||
Priority => priority: i16,
|
||||
LogoOverride => logo_override: bool,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct EpgSourceItemFormProps {
|
||||
pub on_submit: Callback<EpgSourceDto>,
|
||||
pub on_cancel: Callback<()>,
|
||||
#[prop_or_default]
|
||||
pub initial: Option<EpgSourceDto>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn EpgSourceItemForm(props: &EpgSourceItemFormProps) -> Html {
|
||||
let translate = use_translation();
|
||||
|
||||
let form_state: UseReducerHandle<EpgSourceFormState> = use_reducer(|| {
|
||||
EpgSourceFormState {
|
||||
form: props.initial.clone().unwrap_or_else(|| EpgSourceDto {
|
||||
url: String::new(),
|
||||
priority: 0,
|
||||
logo_override: false,
|
||||
}),
|
||||
modified: false,
|
||||
}
|
||||
});
|
||||
|
||||
let handle_submit = {
|
||||
let form_state = form_state.clone();
|
||||
let on_submit = props.on_submit.clone();
|
||||
Callback::from(move |_| {
|
||||
let data = form_state.data().clone();
|
||||
if !data.url.trim().is_empty() {
|
||||
on_submit.emit(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let handle_cancel = {
|
||||
let on_cancel = props.on_cancel.clone();
|
||||
Callback::from(move |_| {
|
||||
on_cancel.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card tp__item-form">
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_EPG_SOURCE_URL), url, EpgSourceFormAction::Url) }
|
||||
|
||||
<div class="tp__item-form__buttons">
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="submit_epg_source"
|
||||
icon="Accept"
|
||||
title={translate.t("LABEL.SUBMIT")}
|
||||
onclick={handle_submit}
|
||||
/>
|
||||
<TextButton
|
||||
class="secondary"
|
||||
name="cancel_epg_source"
|
||||
icon="Cancel"
|
||||
title={translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,10 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
let aliases_state = use_state(|| Vec::<ConfigInputAliasDto>::new());
|
||||
let headers_state = use_state(|| HashMap::<String, String>::new());
|
||||
|
||||
// State for showing item forms
|
||||
let show_epg_form_state = use_state(|| false);
|
||||
let show_alias_form_state = use_state(|| false);
|
||||
|
||||
let staged_input_types = use_memo(staged_input_state.form.input_type, |input_type| {
|
||||
let default_it = input_type;
|
||||
[
|
||||
@@ -339,6 +343,8 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
let headers = headers_state.clone();
|
||||
let epg_sources = epg_sources_state.clone();
|
||||
let aliases = aliases_state.clone();
|
||||
let show_epg_form = show_epg_form_state.clone();
|
||||
let show_alias_form = show_alias_form_state.clone();
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
@@ -360,14 +366,17 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
|
||||
// EPG Sources Section
|
||||
{ config_field_child!(translate.t(LABEL_EPG_SOURCES), {
|
||||
let epg_sources_add = epg_sources.clone();
|
||||
let epg_sources_list = epg_sources.clone();
|
||||
let epg_sources_add = epg_sources.clone();
|
||||
let show_epg_form_toggle = show_epg_form.clone();
|
||||
let show_epg_form_check = show_epg_form.clone();
|
||||
|
||||
html! {
|
||||
<div class="tp__form-list">
|
||||
<div class="tp__form-list__items">
|
||||
{
|
||||
for (*epg_sources).iter().enumerate().map(|(idx, source)| {
|
||||
let epg_sources_remove = epg_sources.clone();
|
||||
for (*epg_sources_list).iter().enumerate().map(|(idx, source)| {
|
||||
let epg_sources_remove = epg_sources_list.clone();
|
||||
html! {
|
||||
<div class="tp__form-list__item" key={idx}>
|
||||
<div class="tp__form-list__item-content">
|
||||
@@ -388,50 +397,48 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div class="tp__form-list__add">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={translate.t(LABEL_EPG_SOURCE_URL)}
|
||||
id="epg-source-input"
|
||||
/>
|
||||
<button
|
||||
onclick={Callback::from(move |_| {
|
||||
if let Some(input) = web_sys::window()
|
||||
.and_then(|w| w.document())
|
||||
.and_then(|d| d.get_element_by_id("epg-source-input"))
|
||||
.and_then(|e| e.dyn_into::<web_sys::HtmlInputElement>().ok())
|
||||
{
|
||||
let url = input.value().trim().to_string();
|
||||
if !url.is_empty() {
|
||||
let mut sources = (*epg_sources_add).clone();
|
||||
sources.push(EpgSourceDto {
|
||||
url,
|
||||
priority: 0,
|
||||
logo_override: false,
|
||||
});
|
||||
epg_sources_add.set(sources);
|
||||
input.set_value("");
|
||||
}
|
||||
}
|
||||
|
||||
if *show_epg_form_check {
|
||||
<EpgSourceItemForm
|
||||
on_submit={Callback::from(move |source: EpgSourceDto| {
|
||||
let mut sources = (*epg_sources_add).clone();
|
||||
sources.push(source);
|
||||
epg_sources_add.set(sources);
|
||||
show_epg_form.set(false);
|
||||
})}
|
||||
>
|
||||
{"+"}
|
||||
</button>
|
||||
</div>
|
||||
on_cancel={Callback::from(move |_| {
|
||||
show_epg_form.set(false);
|
||||
})}
|
||||
/>
|
||||
} else {
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="add_epg_source"
|
||||
icon="Add"
|
||||
title={translate.t(LABEL_ADD_EPG_SOURCE)}
|
||||
onclick={Callback::from(move |_| {
|
||||
show_epg_form_toggle.set(true);
|
||||
})}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
|
||||
// Aliases Section
|
||||
{ config_field_child!(translate.t(LABEL_ALIASES), {
|
||||
let aliases_add = aliases.clone();
|
||||
let aliases_list = aliases.clone();
|
||||
let aliases_add = aliases.clone();
|
||||
let show_alias_form_toggle = show_alias_form.clone();
|
||||
let show_alias_form_check = show_alias_form.clone();
|
||||
let next_id = (*aliases_list).iter().map(|a| a.id).max().unwrap_or(0) + 1;
|
||||
|
||||
html! {
|
||||
<div class="tp__form-list">
|
||||
<div class="tp__form-list__items">
|
||||
{
|
||||
for (*aliases).iter().enumerate().map(|(idx, alias)| {
|
||||
let aliases_remove = aliases.clone();
|
||||
for (*aliases_list).iter().enumerate().map(|(idx, alias)| {
|
||||
let aliases_remove = aliases_list.clone();
|
||||
html! {
|
||||
<div class="tp__form-list__item" key={idx}>
|
||||
<div class="tp__form-list__item-content">
|
||||
@@ -452,48 +459,31 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div class="tp__form-list__add-alias">
|
||||
<input type="text" placeholder={translate.t(LABEL_ALIAS_NAME)} id="alias-name-input" />
|
||||
<input type="text" placeholder={translate.t(LABEL_URL)} id="alias-url-input" />
|
||||
<button
|
||||
onclick={Callback::from(move |_| {
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Some(document) = window.document() {
|
||||
let name_input = document
|
||||
.get_element_by_id("alias-name-input")
|
||||
.and_then(|e| e.dyn_into::<web_sys::HtmlInputElement>().ok());
|
||||
let url_input = document
|
||||
.get_element_by_id("alias-url-input")
|
||||
.and_then(|e| e.dyn_into::<web_sys::HtmlInputElement>().ok());
|
||||
|
||||
if let (Some(name_el), Some(url_el)) = (name_input, url_input) {
|
||||
let name = name_el.value().trim().to_string();
|
||||
let url = url_el.value().trim().to_string();
|
||||
if !name.is_empty() && !url.is_empty() {
|
||||
let mut items = (*aliases_add).clone();
|
||||
let new_id = items.iter().map(|a| a.id).max().unwrap_or(0) + 1;
|
||||
items.push(ConfigInputAliasDto {
|
||||
id: new_id,
|
||||
name,
|
||||
url,
|
||||
username: None,
|
||||
password: None,
|
||||
priority: 0,
|
||||
max_connections: 1,
|
||||
exp_date: None,
|
||||
});
|
||||
aliases_add.set(items);
|
||||
name_el.set_value("");
|
||||
url_el.set_value("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if *show_alias_form_check {
|
||||
<AliasItemForm
|
||||
next_id={next_id}
|
||||
on_submit={Callback::from(move |alias: ConfigInputAliasDto| {
|
||||
let mut items = (*aliases_add).clone();
|
||||
items.push(alias);
|
||||
aliases_add.set(items);
|
||||
show_alias_form.set(false);
|
||||
})}
|
||||
>
|
||||
{"+"}
|
||||
</button>
|
||||
</div>
|
||||
on_cancel={Callback::from(move |_| {
|
||||
show_alias_form.set(false);
|
||||
})}
|
||||
/>
|
||||
} else {
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="add_alias"
|
||||
icon="Add"
|
||||
title={translate.t(LABEL_ADD_ALIAS)}
|
||||
onclick={Callback::from(move |_| {
|
||||
show_alias_form_toggle.set(true);
|
||||
})}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -12,6 +12,9 @@ mod output_m3u_form;
|
||||
mod output_strm_form;
|
||||
mod output_hdhomerun_form;
|
||||
mod layout;
|
||||
mod epg_source_item_form;
|
||||
mod alias_item_form;
|
||||
mod trakt_list_item_form;
|
||||
|
||||
pub(crate) use editor_view::*;
|
||||
pub(crate) use editor_model::*;
|
||||
@@ -25,4 +28,7 @@ pub(crate) use block_view::*;
|
||||
pub(crate) use output_xtream_form::*;
|
||||
pub(crate) use output_m3u_form::*;
|
||||
pub(crate) use output_strm_form::*;
|
||||
pub(crate) use output_hdhomerun_form::*;
|
||||
pub(crate) use output_hdhomerun_form::*;
|
||||
pub(crate) use epg_source_item_form::*;
|
||||
pub(crate) use alias_item_form::*;
|
||||
pub(crate) use trakt_list_item_form::*;
|
||||
@@ -52,18 +52,6 @@ generate_form_reducer!(
|
||||
}
|
||||
);
|
||||
|
||||
generate_form_reducer!(
|
||||
state: TraktListConfigFormState { form: TraktListConfigDto },
|
||||
action_name: TraktListConfigFormAction,
|
||||
fields {
|
||||
User => user: String,
|
||||
ListSlug => list_slug: String,
|
||||
CategoryName => category_name: String,
|
||||
ContentType => content_type: TraktContentType,
|
||||
FuzzyMatchThreshold => fuzzy_match_threshold: u8,
|
||||
}
|
||||
);
|
||||
|
||||
generate_form_reducer!(
|
||||
state: XtreamTargetOutputFormState { form: XtreamTargetOutputDto },
|
||||
action_name: XtreamTargetOutputFormAction,
|
||||
@@ -105,12 +93,8 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
|
||||
// State for Trakt lists
|
||||
let trakt_lists_state = use_state(|| Vec::<TraktListConfigDto>::new());
|
||||
|
||||
// State for adding new Trakt list
|
||||
let trakt_list_add_state: UseReducerHandle<TraktListConfigFormState> =
|
||||
use_reducer(|| TraktListConfigFormState {
|
||||
form: TraktListConfigDto::default(),
|
||||
modified: false,
|
||||
});
|
||||
// State for showing trakt list form
|
||||
let show_trakt_list_form_state = use_state(|| false);
|
||||
|
||||
let view_visible = use_state(|| OutputFormPage::Main.to_string());
|
||||
|
||||
@@ -164,29 +148,7 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
|
||||
let render_trakt = || {
|
||||
let trakt_lists = trakt_lists_state.clone();
|
||||
let trakt_api = trakt_api_state.clone();
|
||||
let trakt_list_add = trakt_list_add_state.clone();
|
||||
|
||||
// Create content type options
|
||||
let content_type_options = use_memo(trakt_list_add.form.content_type, |content_type| {
|
||||
let default_ct = content_type;
|
||||
vec![
|
||||
DropDownOption {
|
||||
id: "vod".to_string(),
|
||||
label: html! { "Vod" },
|
||||
selected: default_ct == &TraktContentType::Vod,
|
||||
},
|
||||
DropDownOption {
|
||||
id: "series".to_string(),
|
||||
label: html! { "Series" },
|
||||
selected: default_ct == &TraktContentType::Series,
|
||||
},
|
||||
DropDownOption {
|
||||
id: "both".to_string(),
|
||||
label: html! { "Both" },
|
||||
selected: default_ct == &TraktContentType::Both,
|
||||
},
|
||||
]
|
||||
});
|
||||
let show_trakt_list_form = show_trakt_list_form_state.clone();
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
@@ -200,13 +162,17 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
|
||||
|
||||
// Trakt Lists
|
||||
{ config_field_child!(translate.t(LABEL_TRAKT_LISTS), {
|
||||
let trakt_lists_list = trakt_lists.clone();
|
||||
let trakt_lists_add = trakt_lists.clone();
|
||||
let show_trakt_list_form_toggle = show_trakt_list_form.clone();
|
||||
let show_trakt_list_form_check = show_trakt_list_form.clone();
|
||||
|
||||
html! {
|
||||
<div class="tp__form-list">
|
||||
<div class="tp__form-list__items">
|
||||
{
|
||||
for (*trakt_lists).iter().enumerate().map(|(idx, list)| {
|
||||
let trakt_lists_remove = trakt_lists.clone();
|
||||
for (*trakt_lists_list).iter().enumerate().map(|(idx, list)| {
|
||||
let trakt_lists_remove = trakt_lists_list.clone();
|
||||
let content_type_str = match list.content_type {
|
||||
TraktContentType::Vod => "Vod",
|
||||
TraktContentType::Series => "Series",
|
||||
@@ -243,47 +209,30 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div class="tp__form-list__add-trakt">
|
||||
{ edit_field_text!(trakt_list_add, translate.t(LABEL_TRAKT_USER), user, TraktListConfigFormAction::User) }
|
||||
{ edit_field_text!(trakt_list_add, translate.t(LABEL_TRAKT_LIST_SLUG), list_slug, TraktListConfigFormAction::ListSlug) }
|
||||
{ edit_field_text!(trakt_list_add, translate.t(LABEL_TRAKT_CATEGORY_NAME), category_name, TraktListConfigFormAction::CategoryName) }
|
||||
{ config_field_child!(translate.t(LABEL_TRAKT_CONTENT_TYPE), {
|
||||
let trakt_list_add_ct = trakt_list_add.clone();
|
||||
html! {
|
||||
<Select
|
||||
name={"trakt_content_type"}
|
||||
multi_select={false}
|
||||
on_select={Callback::from(move |(_, selections):(String, DropDownSelection)| {
|
||||
match selections {
|
||||
DropDownSelection::Single(option) => {
|
||||
if let Ok(ct) = option.parse::<TraktContentType>() {
|
||||
trakt_list_add_ct.dispatch(TraktListConfigFormAction::ContentType(ct));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})}
|
||||
options={content_type_options.clone()}
|
||||
/>
|
||||
}
|
||||
})}
|
||||
{ edit_field_number_u8!(trakt_list_add, translate.t(LABEL_TRAKT_FUZZY_MATCH_THRESHOLD), fuzzy_match_threshold, TraktListConfigFormAction::FuzzyMatchThreshold) }
|
||||
<button
|
||||
class="tp__form-list__add-button"
|
||||
onclick={Callback::from(move |_| {
|
||||
let list_data = trakt_list_add.data();
|
||||
if !list_data.user.is_empty() && !list_data.list_slug.is_empty() && !list_data.category_name.is_empty() {
|
||||
let mut items = (*trakt_lists_add).clone();
|
||||
items.push(list_data.clone());
|
||||
trakt_lists_add.set(items);
|
||||
// Reset form
|
||||
trakt_list_add.dispatch(TraktListConfigFormAction::SetAll(TraktListConfigDto::default()));
|
||||
}
|
||||
|
||||
if *show_trakt_list_form_check {
|
||||
<TraktListItemForm
|
||||
on_submit={Callback::from(move |list: TraktListConfigDto| {
|
||||
let mut items = (*trakt_lists_add).clone();
|
||||
items.push(list);
|
||||
trakt_lists_add.set(items);
|
||||
show_trakt_list_form.set(false);
|
||||
})}
|
||||
>
|
||||
{translate.t(LABEL_ADD_TRAKT_LIST)}
|
||||
</button>
|
||||
</div>
|
||||
on_cancel={Callback::from(move |_| {
|
||||
show_trakt_list_form.set(false);
|
||||
})}
|
||||
/>
|
||||
} else {
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="add_trakt_list"
|
||||
icon="Add"
|
||||
title={translate.t(LABEL_ADD_TRAKT_LIST)}
|
||||
onclick={Callback::from(move |_| {
|
||||
show_trakt_list_form_toggle.set(true);
|
||||
})}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
use crate::app::components::select::Select;
|
||||
use crate::app::components::{Card, DropDownOption, DropDownSelection, TextButton};
|
||||
use crate::{config_field_child, edit_field_number_u8, edit_field_text, generate_form_reducer};
|
||||
use shared::model::{TraktContentType, TraktListConfigDto};
|
||||
use yew::{function_component, html, use_memo, use_reducer, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_TRAKT_USER: &str = "LABEL.TRAKT_USER";
|
||||
const LABEL_TRAKT_LIST_SLUG: &str = "LABEL.TRAKT_LIST_SLUG";
|
||||
const LABEL_TRAKT_CATEGORY_NAME: &str = "LABEL.TRAKT_CATEGORY_NAME";
|
||||
const LABEL_TRAKT_CONTENT_TYPE: &str = "LABEL.TRAKT_CONTENT_TYPE";
|
||||
const LABEL_TRAKT_FUZZY_MATCH_THRESHOLD: &str = "LABEL.TRAKT_FUZZY_MATCH_THRESHOLD";
|
||||
|
||||
generate_form_reducer!(
|
||||
state: TraktListFormState { form: TraktListConfigDto },
|
||||
action_name: TraktListFormAction,
|
||||
fields {
|
||||
User => user: String,
|
||||
ListSlug => list_slug: String,
|
||||
CategoryName => category_name: String,
|
||||
ContentType => content_type: TraktContentType,
|
||||
FuzzyMatchThreshold => fuzzy_match_threshold: u8,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct TraktListItemFormProps {
|
||||
pub on_submit: Callback<TraktListConfigDto>,
|
||||
pub on_cancel: Callback<()>,
|
||||
#[prop_or_default]
|
||||
pub initial: Option<TraktListConfigDto>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn TraktListItemForm(props: &TraktListItemFormProps) -> Html {
|
||||
let translate = use_translation();
|
||||
|
||||
let form_state: UseReducerHandle<TraktListFormState> = use_reducer(|| {
|
||||
TraktListFormState {
|
||||
form: props.initial.clone().unwrap_or_else(|| TraktListConfigDto {
|
||||
user: String::new(),
|
||||
list_slug: String::new(),
|
||||
category_name: String::new(),
|
||||
content_type: TraktContentType::Both,
|
||||
fuzzy_match_threshold: 80,
|
||||
}),
|
||||
modified: false,
|
||||
}
|
||||
});
|
||||
|
||||
let content_type_options = use_memo(form_state.form.content_type, |content_type| {
|
||||
let default_ct = content_type;
|
||||
vec![
|
||||
DropDownOption {
|
||||
id: "vod".to_string(),
|
||||
label: html! { "Vod" },
|
||||
selected: default_ct == &TraktContentType::Vod,
|
||||
},
|
||||
DropDownOption {
|
||||
id: "series".to_string(),
|
||||
label: html! { "Series" },
|
||||
selected: default_ct == &TraktContentType::Series,
|
||||
},
|
||||
DropDownOption {
|
||||
id: "both".to_string(),
|
||||
label: html! { "Both" },
|
||||
selected: default_ct == &TraktContentType::Both,
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
let handle_submit = {
|
||||
let form_state = form_state.clone();
|
||||
let on_submit = props.on_submit.clone();
|
||||
Callback::from(move |_| {
|
||||
let data = form_state.data().clone();
|
||||
if !data.user.trim().is_empty()
|
||||
&& !data.list_slug.trim().is_empty()
|
||||
&& !data.category_name.trim().is_empty() {
|
||||
on_submit.emit(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let handle_cancel = {
|
||||
let on_cancel = props.on_cancel.clone();
|
||||
Callback::from(move |_| {
|
||||
on_cancel.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card tp__item-form">
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_TRAKT_USER), user, TraktListFormAction::User) }
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_TRAKT_LIST_SLUG), list_slug, TraktListFormAction::ListSlug) }
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_TRAKT_CATEGORY_NAME), category_name, TraktListFormAction::CategoryName) }
|
||||
|
||||
{ config_field_child!(translate.t(LABEL_TRAKT_CONTENT_TYPE), {
|
||||
let form_state_ct = form_state.clone();
|
||||
html! {
|
||||
<Select
|
||||
name={"trakt_content_type"}
|
||||
multi_select={false}
|
||||
on_select={Callback::from(move |(_, selections):(String, DropDownSelection)| {
|
||||
match selections {
|
||||
DropDownSelection::Single(option) => {
|
||||
if let Ok(ct) = option.parse::<TraktContentType>() {
|
||||
form_state_ct.dispatch(TraktListFormAction::ContentType(ct));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})}
|
||||
options={content_type_options.clone()}
|
||||
/>
|
||||
}
|
||||
})}
|
||||
|
||||
{ edit_field_number_u8!(form_state, translate.t(LABEL_TRAKT_FUZZY_MATCH_THRESHOLD), fuzzy_match_threshold, TraktListFormAction::FuzzyMatchThreshold) }
|
||||
|
||||
<div class="tp__item-form__buttons">
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="submit_trakt_list"
|
||||
icon="Accept"
|
||||
title={translate.t("LABEL.SUBMIT")}
|
||||
onclick={handle_submit}
|
||||
/>
|
||||
<TextButton
|
||||
class="secondary"
|
||||
name="cancel_trakt_list"
|
||||
icon="Cancel"
|
||||
title={translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user