diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index 181ded960..e2817dca1 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -189,7 +189,7 @@ "RENAME_SETTINGS": "Rename Settings", "PATTERN": "Pattern", "NEW_NAME": "New Name", - "SKIP": "Skip", + "SKIP": "Skip", "LIVE_STREAM_USE_PREFIX": "Live use prefix", "LIVE_STREAM_WITHOUT_EXTENSION": "Live w/o ext.", "ALIAS": "Alias", @@ -201,12 +201,12 @@ "COUNTER": "Counter", "MAPPING_PATH": "Mapping dir", "USER_CONFIG_DIR": "User Config dir", - "USER_ACCESS_CONTROL": "User Access Control", + "USER_ACCESS_CONTROL": "User Access Control", "SLEEP_TIMER_MINS": "Sleep Timer (mins)", - "CONNECT_TIMEOUT_SECS": "Connect Timeout (secs)", + "CONNECT_TIMEOUT_SECS": "Connect Timeout (secs)", "CONFIG_HOT_RELOAD": "Config Hot Reload", - "CUSTOM_STREAM_RESPONSE_PATH": "Custom Stream Response dir", - "REVERSE_PROXY" : "Reverse Proxy", + "CUSTOM_STREAM_RESPONSE_PATH": "Custom Stream Response dir", + "REVERSE_PROXY": "Reverse Proxy", "WEB_SEARCH": "Web Search", "USER_UI_ENABLED": "User-UI Enabled", "RESOURCE_REWRITE_DISABLED": "Resource Rewrite Disabled", @@ -220,13 +220,13 @@ "THROTTLE_KBPS": "Throttle kbps", "FRIENDLY_NAME": "Friendly Name", "MANUFACTURER": "Manufacturer", - "MODEL_NAME": "Model Name", + "MODEL_NAME": "Model Name", "MODEL_NUMBER": "Model Number", - "FIRMWARE_NAME": "Firmware Name", + "FIRMWARE_NAME": "Firmware Name", "FIRMWARE_VERSION": "Firmware Version", - "DEVICE_TYPE": "Device Type", + "DEVICE_TYPE": "Device Type", "DEVICE_UDN": "Device UDN", - "TUNER_COUNT": "Tuner Count", + "TUNER_COUNT": "Tuner Count", "SOURCE": "Source", "PRIORITY": "Priority", "LOGO_OVERRIDE": "logo Override", @@ -238,12 +238,12 @@ "BEST_MATCH_THRESHOLD": "Best Match Threshold", "NORMALIZE_REGEX": "Normalize Regex", "NAME_PREFIX": "Name Prefix", - "NAME_PREFIX_SEPARATOR":"Name Prefix Separator", + "NAME_PREFIX_SEPARATOR": "Name Prefix Separator", "STRIP": "Strip", "COPY_LINK_TULIPROX_VIRTUAL_ID": "Copy Virtual Id", "COPY_LINK_TULIPROX_WEBPLAYER_URL": "Copy Webplayer Url", "COPY_LINK_PROVIDER_URL": "Copy Provider Url", - "CONTENT_SECURITY_POLICY" : "Content Security Policy", + "CONTENT_SECURITY_POLICY": "Content Security Policy", "CUSTOM_ATTRIBUTES": "Custom Attributes", "EMPTY": "", "OPTIONS": "Options", @@ -260,12 +260,18 @@ "ALIASES": "Aliases", "MAX_CONNECTIONS": "Max Connections", "METHOD": "Method", - "EPG_TIMESHIFT": "Epg Timeshift", - "CREATED_AT": "Created at", - "EXP_DATE": "Exp. date", - "UI_ENABLED": "Ui", + "EPG_TIMESHIFT": "Epg Timeshift", + "CREATED_AT": "Created at", + "EXP_DATE": "Exp. date", + "UI_ENABLED": "Ui", "COMMENT": "Comment", - "STAGED": "Staged" + "STAGED": "Staged", + "USER_STATUS_ACTIVE": "Active", + "USER_STATUS_EXPIRED": "Expired", + "USER_STATUS_BANNED": "Banned", + "USER_STATUS_TRIAL": "Trial", + "USER_STATUS_DISABLED": "Disabled", + "USER_STATUS_PENDING": "Pending" }, "TITLE": { "USER_BOUQUET_EDITOR": "User group editor" @@ -276,7 +282,7 @@ "INVALID_FILETYPE": "Invalid filetype", "TARGET_NOT_EXISTS": "Target does not exist", "CONFIRM_DELETE": "You really want to delete?", - "CONFIG_CHANGED": "Configuration on server changed.", + "CONFIG_CHANGED": "Configuration on server changed.", "DOWNLOAD": { "SUCCESS": "Successfully downloaded", "FAIL": "Failed to download!", diff --git a/frontend/scss/app/_component.scss b/frontend/scss/app/_component.scss index b69addab8..f673ac791 100644 --- a/frontend/scss/app/_component.scss +++ b/frontend/scss/app/_component.scss @@ -66,3 +66,4 @@ @forward "components/config/config_view"; @forward "components/tabset"; @forward "components/form"; +@forward "components/select"; \ No newline at end of file diff --git a/frontend/scss/app/components/_select.scss b/frontend/scss/app/components/_select.scss new file mode 100644 index 000000000..5a87fd0f6 --- /dev/null +++ b/frontend/scss/app/components/_select.scss @@ -0,0 +1,45 @@ +.tp__select { + display: flex; + flex-flow: column; + overflow: hidden; + box-sizing: border-box; + gap: 6px; + + &-wrapper { + fill: var(--menu-icon-color); + display: flex; + flex-flow: row nowrap; + align-items: center; + overflow: hidden; + box-sizing: border-box; + padding-left: var(--padding-mini); + border: 1px solid var(--input-border-color); + border-radius: var(--border-radius); + background-color: var(--input-background-color); + &:focus-within { + border-color: var(--input-focus-border-color); + } + + .svg-icon { + height: 2rem; + width: 2rem; + } + + .tp__icon-button { + .svg-icon { + height: 1.5rem; + width: 1.5rem; + } + } + } + label { + padding-left: 2px; + color: var(--text-color); + } + + &__selected { + display: flex; + flex-flow: row nowrap; + flex: 1 1 auto; + } +} diff --git a/frontend/src/app/components/config/macros.rs b/frontend/src/app/components/config/macros.rs index c016adc67..c21a921fc 100644 --- a/frontend/src/app/components/config/macros.rs +++ b/frontend/src/app/components/config/macros.rs @@ -105,33 +105,6 @@ macro_rules! config_field_empty { }; } -#[macro_export] -macro_rules! edit_field_text { - ($instance:expr, $label:expr, $field:ident) => { - $crate::edit_field_text!(@inner $instance, $label, $field, false) - }; - ($instance:expr, $label:expr, $field:ident, $hidden:expr) => { - $crate::edit_field_text!(@inner $instance, $label, $field, $hidden) - }; - (@inner $instance:expr, $label:expr, $field:ident, $hidden:expr) => {{ - let instance = $instance.clone(); - html! { -
- <$crate::app::components::input::Input - label={$label} - hidden={$hidden} - name={stringify!($field)} - autocomplete={true} - value={instance.borrow().$field.clone()} - ontext={Callback::from(move |value: String| { - instance.borrow_mut().$field = value; - })} - /> -
- } - }}; -} - #[macro_export] macro_rules! edit_field_text_option { ($instance:expr, $label:expr, $field:ident) => { @@ -163,6 +136,33 @@ macro_rules! edit_field_text_option { }}; } +#[macro_export] +macro_rules! edit_field_text { + ($instance:expr, $label:expr, $field:ident) => { + $crate::edit_field_text!(@inner $instance, $label, $field, false) + }; + ($instance:expr, $label:expr, $field:ident, $hidden:expr) => { + $crate::edit_field_text!(@inner $instance, $label, $field, $hidden) + }; + (@inner $instance:expr, $label:expr, $field:ident, $hidden:expr) => {{ + let instance = $instance.clone(); + html! { +
+ <$crate::app::components::input::Input + label={$label} + hidden={$hidden} + name={stringify!($field)} + autocomplete={true} + value={instance.borrow().$field.clone()} + ontext={Callback::from(move |value: String| { + instance.borrow_mut().$field = value; + })} + /> +
+ } + }}; +} + // #[macro_export] diff --git a/frontend/src/app/components/drop_down_icon_button.rs b/frontend/src/app/components/drop_down_icon_button.rs index 002e58a2c..21140eb32 100644 --- a/frontend/src/app/components/drop_down_icon_button.rs +++ b/frontend/src/app/components/drop_down_icon_button.rs @@ -10,9 +10,9 @@ use crate::html_if; #[derive(Clone, PartialEq, Debug)] pub struct DropDownOption { - id: String, - label: String, - selected: bool, + pub(crate) id: String, + pub(crate) label: String, + pub(crate) selected: bool, } impl DropDownOption { @@ -31,7 +31,6 @@ pub struct DropDownIconButtonProps { pub options: Rc>, #[prop_or_default] pub multi_select: bool, - } #[function_component] diff --git a/frontend/src/app/components/mod.rs b/frontend/src/app/components/mod.rs index c8f3c5a95..395d9eeb4 100644 --- a/frontend/src/app/components/mod.rs +++ b/frontend/src/app/components/mod.rs @@ -35,6 +35,7 @@ mod accordion_panel; mod csv_table; mod config; mod tabset; +mod select; // pub use self::input::*; // pub use self::menu_item::*; // pub use self::popup_menu::*; diff --git a/frontend/src/app/components/select.rs b/frontend/src/app/components/select.rs new file mode 100644 index 000000000..34a6b0ab7 --- /dev/null +++ b/frontend/src/app/components/select.rs @@ -0,0 +1,69 @@ +use std::rc::Rc; +use log::warn; +use yew::prelude::*; +use crate::app::components::{DropDownIconButton, DropDownOption, Tag, TagList}; + +#[derive(Properties, Clone, PartialEq, Debug)] +pub struct SelectProps { + pub name: String, + #[prop_or_default] + pub icon: Option, + pub onselect: Callback<(String, Vec)>, + #[prop_or_default] + pub class: String, + pub options: Rc>, + #[prop_or_default] + pub multi_select: bool, +} + +fn create_tag_from_str(s: &str) -> Rc { + Rc::new(Tag { label: s.to_owned(), class: Some("active".to_string()) }) +} + +fn create_tag_from_option(o: &DropDownOption) -> Rc { + create_tag_from_str(o.label.as_str()) +} + +#[function_component] +pub fn Select(props: &SelectProps) -> Html { + + // TODO render for selections !!! + + let selected_options = use_state(|| props.options.as_ref().iter().filter(|o| o.selected) + .map(create_tag_from_option) + .collect::>>()); + { + let set_selected_options = selected_options.clone(); + use_effect_with(props.options.clone(), move |options| { + let selections = options.as_ref().iter().filter(|o| o.selected) + .map(create_tag_from_option) + .collect::>>(); + set_selected_options.set(selections); + }); + } + + let handle_options_click = { + let onselect = props.onselect.clone(); + let set_selections = selected_options.clone(); + Callback::from(move |(name, selections): (String, Vec)| { + set_selections.set(selections.iter().map(|s|s.as_str()).map(create_tag_from_str).collect::>>()); + onselect.emit((name, selections)); + }) + }; + + html! { +
+
+
+ +
+ +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/tag_list.rs b/frontend/src/app/components/tag_list.rs index 8a3f73dbe..0fa3e7761 100644 --- a/frontend/src/app/components/tag_list.rs +++ b/frontend/src/app/components/tag_list.rs @@ -32,6 +32,14 @@ pub fn tag_list(props: &TagListProps) -> Html { let tag_state = use_state(|| tags.clone()); let new_tag = use_state(String::default); + { + let tag_state = tag_state.clone(); + use_effect_with(tags.clone(), move |tags| { + tag_state.set(tags.clone()); + || () + }); + } + let on_remove = { let tag_state = tag_state.clone(); let on_change = on_change.clone(); diff --git a/frontend/src/app/components/userlist/proxy_user_credentials_form.rs b/frontend/src/app/components/userlist/proxy_user_credentials_form.rs index 1baf13af0..674695016 100644 --- a/frontend/src/app/components/userlist/proxy_user_credentials_form.rs +++ b/frontend/src/app/components/userlist/proxy_user_credentials_form.rs @@ -3,10 +3,11 @@ use std::rc::Rc; use log::warn; use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::ProxyUserCredentialsDto; +use shared::model::{ProxyUserCredentialsDto, ProxyUserStatus}; use crate::app::TargetUser; use crate::{edit_field_bool, edit_field_text, edit_field_text_option}; -use crate::app::components::TextButton; +use crate::app::components::select::Select; +use crate::app::components::{convert_bool_to_chip_style, Chip, DropDownOption, TextButton}; #[derive(Properties, PartialEq, Clone)] pub struct ProxyUserCredentialsFormProps { @@ -16,18 +17,52 @@ pub struct ProxyUserCredentialsFormProps { #[function_component] pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html { let translate = use_translation(); - let form_state = use_memo(props.user.clone(), |user| Rc::new(RefCell::new(user.as_ref().map_or_else(|| ProxyUserCredentialsDto::default(), |usr| usr.credentials.as_ref().clone())))); + let form_state = use_memo(props.user.clone(), + |user| RefCell::new(user.as_ref() + .map_or_else(|| ProxyUserCredentialsDto::default(), |usr| usr.credentials.as_ref().clone()))); + + let proxy_user_status = use_memo(props.user.clone(), |user| + vec![ + ProxyUserStatus::Active, + ProxyUserStatus::Expired, + ProxyUserStatus::Banned, + ProxyUserStatus::Trial, + ProxyUserStatus::Disabled, + ProxyUserStatus::Pending, + ].iter().map(|s| DropDownOption { + id: s.to_string(), + label: format!("LABEL.USER_STATUS_{}", s.to_string().to_uppercase()), + selected: user.as_ref().is_some_and(|user| user.credentials.status.as_ref() == Some(s)), + }).collect::>() + ); let handle_save_user = { - let user = form_state.clone(); + let user = form_state.clone(); Callback::from(move |_| { warn!("{:?}", user.borrow()); }) }; + let user_active = props.user.as_ref().is_some_and(|u| u.credentials.is_active()); + html! {
+
+ +
+
+ +