From 04c44c0b483931e1a109e99a517e9f03523503ae Mon Sep 17 00:00:00 2001 From: euzu Date: Wed, 6 Aug 2025 12:32:36 +0200 Subject: [PATCH 1/6] Mapper script view --- shared/src/foundation/mapper.rs | 68 +++-- webui/public/assets/i18n/en.json | 3 +- webui/scss/app/_component.scss | 1 + webui/scss/app/components/_mapper_script.scss | 103 +++++++ .../playlist/_playlist_mappings.scss | 48 +++- webui/src/app/components/custom_dialog.rs | 4 +- .../components/playlist/mapper_script_view.rs | 267 ++++++++++++++++++ webui/src/app/components/playlist/mappings.rs | 46 ++- webui/src/app/components/playlist/mod.rs | 2 + webui/src/services/config_service.rs | 15 + 10 files changed, 516 insertions(+), 41 deletions(-) create mode 100644 webui/scss/app/components/_mapper_script.scss create mode 100644 webui/src/app/components/playlist/mapper_script_view.rs diff --git a/shared/src/foundation/mapper.rs b/shared/src/foundation/mapper.rs index 054c10475..3912574f1 100644 --- a/shared/src/foundation/mapper.rs +++ b/shared/src/foundation/mapper.rs @@ -8,6 +8,8 @@ use regex::Regex; use std::borrow::Cow; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::ops::Deref; use std::str::FromStr; use pest::Parser; use crate::error::{TuliproxError, TuliproxErrorKind, create_tuliprox_error_result, info_err}; @@ -58,22 +60,30 @@ main = { SOI ~ statements? ~ EOI } struct MapperParser; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct ExprId(pub usize); +pub struct ExprId(pub usize); + +impl Deref for ExprId { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} #[derive(Debug, Clone, PartialEq)] -enum MatchCaseKey { +pub enum MatchCaseKey { Identifier(String), AnyMatch, } #[derive(Debug, Clone, PartialEq)] -struct MatchCase { +pub struct MatchCase { pub keys: Vec, pub expression: ExprId, } #[derive(Debug, Clone, PartialEq)] -enum MapCaseKey { +pub enum MapCaseKey { Text(String), RangeFrom(f64), RangeTo(f64), @@ -83,13 +93,13 @@ enum MapCaseKey { } #[derive(Debug, Clone, PartialEq)] -struct MapCase { +pub struct MapCase { pub keys: Vec, pub expression: ExprId, } #[derive(Debug, Clone, PartialEq)] -enum MapKey { +pub enum MapKey { Identifier(String), FieldAccess(String), VarAccess(String, String), @@ -97,7 +107,7 @@ enum MapKey { #[derive(Debug, Clone, PartialEq)] -enum BuiltInFunction { +pub enum BuiltInFunction { Concat, Uppercase, Lowercase, @@ -130,14 +140,32 @@ impl FromStr for BuiltInFunction { } } +impl Display for BuiltInFunction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match &self { + Self::Concat => "concat", + Self::Capitalize => "capitalize", + Self::Lowercase => "lowercase", + Self::Uppercase => "uppercase", + Self::Trim => "trim", + Self::Print => "print", + Self::ToNumber => "number", + Self::First => "first", + Self::Template => "template", + Self::Replace => "replace", + }.to_owned(); + write!(f, "{}", str) + } +} + #[derive(Debug, Clone, PartialEq)] -enum RegexSource { +pub enum RegexSource { Identifier(String), Field(String), } #[derive(Debug, Clone)] -enum Expression { +pub enum Expression { Identifier(String), StringLiteral(String), NumberLiteral(f64), @@ -178,21 +206,21 @@ impl PartialEq for Expression { #[derive(Debug, Clone, PartialEq)] -enum AssignmentTarget { +pub enum AssignmentTarget { Identifier(String), Field(String), } #[derive(Debug, Clone, PartialEq)] -enum Statement { +pub enum Statement { Expression(ExprId), - Comment, //(String), + Comment(String), } #[derive(Debug, Clone, PartialEq)] pub struct MapperScript { - expressions: Vec, - statements: Vec, + pub expressions: Vec, + pub statements: Vec, } impl MapperScript { @@ -206,6 +234,10 @@ impl MapperScript { stmt.eval(ctx, setter); } } + + pub fn get_expr_by_id(&self, id: usize) -> Option<&Expression> { + self.expressions.get(id) + } } impl ExprId { @@ -226,7 +258,7 @@ impl Statement { // trace!("Ignoring result {result:?}"); } } - Statement::Comment => {} + Statement::Comment(_) => {} } } } @@ -241,7 +273,7 @@ impl MapperScript { Statement::Expression(expr) => { ctx.validate_expr(*expr, &mut identifiers)?; } - Statement::Comment => {} + Statement::Comment(_) => {} } } Ok(()) @@ -272,7 +304,7 @@ impl MapperScript { Ok(None) } } - Rule::comment => Ok(Some(Statement::Comment /*(pair.as_str().trim().to_string())*/)), + Rule::comment => Ok(Some(Statement::Comment(pair.as_str().trim().to_string()))), _ => { // error!("Unknown statement rule: {:?}", pair.as_rule()); @@ -767,7 +799,7 @@ impl<'a> MapperContext<'a> { } #[derive(Debug, Clone)] -enum EvalResult { +pub enum EvalResult { Undefined, Value(String), Number(f64), diff --git a/webui/public/assets/i18n/en.json b/webui/public/assets/i18n/en.json index d6f39d154..0455afc11 100644 --- a/webui/public/assets/i18n/en.json +++ b/webui/public/assets/i18n/en.json @@ -195,7 +195,8 @@ "ALIAS": "Alias", "INPUTS": "Inputs", "NEW_USER": "New User", - "USERLIST": "Userlist" + "USERLIST": "Userlist", + "MAPPER": "Mapper" }, "TABLE": { "EMPTY": "", diff --git a/webui/scss/app/_component.scss b/webui/scss/app/_component.scss index a85c7780a..607dd1772 100644 --- a/webui/scss/app/_component.scss +++ b/webui/scss/app/_component.scss @@ -48,6 +48,7 @@ @forward "components/dialog"; @forward "components/custom_dialog"; @forward "components/filter"; +@forward "components/mapper_script"; @forward "components/userlist/user_state"; @forward "components/userlist/proxy_type"; @forward "components/userlist/max_connections"; diff --git a/webui/scss/app/components/_mapper_script.scss b/webui/scss/app/components/_mapper_script.scss new file mode 100644 index 000000000..02ec7d246 --- /dev/null +++ b/webui/scss/app/components/_mapper_script.scss @@ -0,0 +1,103 @@ +.tp__mapper-script { + display: flex; + flex-flow: column; + overflow: auto; + + .tp__mapper-script__code { + font-family: monospace; + background: #1e1e1e; + color: #d4d4d4; + padding: 1rem; + border-radius: 8px; + white-space: pre; + + .identifier { + color: #569cd6; + } + + .var-access { + color: #9cdcfe; + } + + .field { + color: #fdb76c; + } + + .property { + color: #9a652f; + } + + .literal { + color: #5fcc5c; + } + + .num-literal { + color: #429140; + } + + .null-literal { + border: 1px solid var(--border-color); + padding: var(--padding-micro); + border-radius: var(--border-radius); + color: var(--modest-text-color); + background-color: var(--card-background-color); + } + + .built-in-function { + + } + + //.binary_op-wrapper{ + // white-space: pre; + //} + // + //.comparison { + // white-space: nowrap; + // overflow: hidden; + // background-color: #202429; + // padding: var(--padding-micro); + // border-radius: var(--border-radius); + //} + // + //.unary_op { + // font-weight: bold; + // color: #569cd6; + // text-transform: uppercase; + //} + // + //.binary_op { + // font-weight: bold; + // color: #9cdcfe; + // text-transform: uppercase; + //} + // + //.field { + // font-weight: bold; + // color: #fdb76c; + //} + // + //.regex { + // color: #bb8953; + // white-space: nowrap; + //} + // + //.enum { + // color: #78ce89; + //} + } + + .bracket { + font-weight: bold; + font-size: 1.1rem; + } + + .bracket-1 { color: #569CD6; } + .bracket-2 { color: #C586C0; } + .bracket-3 { color: #B5CEA8; } +} +.tp__mapper-script__inline { + .tp__mapper-script__code { + padding: 0; + margin-block: 0; + } +} \ No newline at end of file diff --git a/webui/scss/app/components/playlist/_playlist_mappings.scss b/webui/scss/app/components/playlist/_playlist_mappings.scss index c0b8e1691..60fc2ca16 100644 --- a/webui/scss/app/components/playlist/_playlist_mappings.scss +++ b/webui/scss/app/components/playlist/_playlist_mappings.scss @@ -1,20 +1,42 @@ .tp__playlist-mappings { - li { - display: flex; - flex-flow: row nowrap; - gap: var(--gap-small); - align-items: center; - background: transparent; - white-space: nowrap; - padding: var(--padding-micro) var(--padding-mini); - border-radius: var(--border-radius); + //li { + // display: flex; + // flex-flow: row nowrap; + // gap: var(--gap-small); + // align-items: center; + // background: transparent; + // white-space: nowrap; + // padding: var(--padding-micro) var(--padding-mini); + // border-radius: var(--border-radius); + // + // &:hover { + // cursor: pointer; + // background-color: var(--menu-item-hover-background-color); + // color: var(--menu-item-hover-color) + // } + //} + + &__mapping { + display: flex; + flex-flow: column; + gap: var(--gap-default); + + &-section { + display: flex; + flex-flow: row; + align-items: center; + gap: var(--gap-small); - &:hover { - cursor: pointer; - background-color: var(--menu-item-hover-background-color); - color: var(--menu-item-hover-color) } } + label { + font-weight: bold; + color: var(--modest-text-color); + + &:after { + content: ':' + } + } } \ No newline at end of file diff --git a/webui/src/app/components/custom_dialog.rs b/webui/src/app/components/custom_dialog.rs index cdfc45eb9..c7c49b1f2 100644 --- a/webui/src/app/components/custom_dialog.rs +++ b/webui/src/app/components/custom_dialog.rs @@ -1,4 +1,3 @@ -use log::error; use yew::prelude::*; use web_sys::MouseEvent; @@ -31,10 +30,9 @@ pub fn CustomDialog(props: &CustomDialogProps) -> Html { // Handle backdrop click let on_backdrop_click = { let on_close = props.on_close.clone(); - let is_open = is_open.clone(); let close_on_backdrop = props.close_on_backdrop_click; - Callback::from(move |e: MouseEvent| { + Callback::from(move |_e: MouseEvent| { if close_on_backdrop { if let Some(on_close) = &on_close { on_close.emit(()); diff --git a/webui/src/app/components/playlist/mapper_script_view.rs b/webui/src/app/components/playlist/mapper_script_view.rs new file mode 100644 index 000000000..d8c7c07f5 --- /dev/null +++ b/webui/src/app/components/playlist/mapper_script_view.rs @@ -0,0 +1,267 @@ +use std::ops::Deref; +use regex::Regex; +use shared::foundation::mapper::{MapperScript, Statement, Expression, ExprId, MapKey, BuiltInFunction, MapCase, AssignmentTarget, MatchCase, RegexSource, MapCaseKey}; +use yew::prelude::*; + +#[derive(Properties, PartialEq, Clone)] +pub struct MapperScriptViewProps { + #[prop_or_default] + pub pretty: bool, + #[prop_or(false)] + pub inline: bool, + pub script: Option, +} + +#[function_component] +pub fn MapperScriptView(props: &MapperScriptViewProps) -> Html { + html! { +
+ { + match props.script.as_ref() { + Some(script) => html! { +
+                            { render_script(script, props.pretty, 0, false, 1) }
+                        
+ }, + None => html! { }, + } + } +
+ } +} + +struct FormatParams { + pub pretty: bool, + pub level: usize, + pub do_indent: bool, + pub p_count: usize, +} + +// Indents with spaces for pretty printing +fn indent(level: usize, do_indent: bool) -> Html { + if do_indent { + let spaces: AttrValue = " ".repeat(level * 2).into(); + html! { <>{ spaces } } + } else { + html! {} + } +} + +fn newline(format_params: &FormatParams) -> Html { + if format_params.pretty { + html! {
} + } else { + html!{} + } +} + +fn render_args(args: &[ExprId], script: &MapperScript, format_params: &FormatParams) -> Html { + html! { + <> + { + for args.iter().map(|expr_id| { + render_expression(expr_id, script, format_params) + }) + } + + } +} + +fn render_var_access(name: &str, field: &str) -> Html { + html! { { name }{ field } } +} + +fn render_field(field: &str) -> Html { + html! { {"@"}{ field } } +} + +fn render_identifier(ident: &str) -> Html { + html! { { ident } } +} + +fn render_map_key(key: &MapKey) -> Html { + match key { + MapKey::Identifier(ident) => render_identifier(ident), + MapKey::FieldAccess(field) => render_field(field), + MapKey::VarAccess(name, field) => render_var_access(name, field), + } +} + +fn render_function_call(name: &BuiltInFunction, args: &[ExprId], script: &MapperScript, format_params: &FormatParams) -> Html { + html! { + {name.to_string()}{"("}{render_args(args, script, format_params)}{")"} + } +} + +fn render_literal(literal: &str) -> Html { + html! { {"'"}{ literal }{"'"} } +} + +fn render_num_literal(literal: &f64) -> Html { + html! { { literal } } +} + +fn render_null_value() -> Html { + html! { { "null" } } +} + +fn render_map_case(case: &MapCase, script: &MapperScript, format_params: &FormatParams) -> Html { + let keys_html = html! { + <> + { + for case.keys.iter().enumerate().map(|(i, key)| { + let item = match key { + MapCaseKey::Text(text) => render_literal(text), + MapCaseKey::RangeFrom(from) => html! { format!("{from}..") }, + MapCaseKey::RangeTo(to) => html! { format!("..{to}") }, + MapCaseKey::RangeFull(from, to) => html! { format!("{from}..{to}") }, + MapCaseKey::RangeEq(val) => html! { val.to_string() }, + MapCaseKey::AnyMatch => html! { "_" }, + }; + + if i < case.keys.len() - 1 { + html! { <> { item } { ", " } } + } else { + html! { { item } } + } + }) + } + + }; + let has_bracket = case.keys.len() > 1; + html! { + <> + {if has_bracket {"("} else {""}} + {keys_html} + {if has_bracket {")"} else {""}} + {" => "} {render_expression(&case.expression, script, format_params)}{","} + {newline(format_params)} + + } +} + + +fn render_map_cases(cases: &[MapCase], script: &MapperScript, format_params: &FormatParams) -> Html { + html!{ + <> + { + for cases.iter().map(|case| render_map_case(case, script, format_params)) + } + + } +} + +fn render_map_block(map_key: &MapKey, cases: &[MapCase], script: &MapperScript, format_params: &FormatParams) -> Html { + html! { + <> {"map "} + {render_map_key(map_key)} + {" {"} + {newline(format_params)} + {render_map_cases(cases, script, format_params)} + {newline(format_params)} + {"}"} + {newline(format_params)} + + } +} + + +fn render_block(expr_ids: &[ExprId], script: &MapperScript, format_params: &FormatParams) -> Html { + html! { + <> + {"{"} + { newline(format_params) } + { + for expr_ids.iter().map(|expr_id| { + html! { + <> + {render_expression(expr_id, script, format_params)} + {newline(format_params)} + + }}) + } + { newline(format_params) } + {"}"} + {newline(format_params)} + + + } +} + +fn render_assignment(target: &AssignmentTarget, expr_id: &ExprId, script: &MapperScript, format_params: &FormatParams) -> Html { + let target_html = match target { + AssignmentTarget::Identifier(ident) => render_identifier(ident), + AssignmentTarget::Field(field) => render_field(field), + }; + + html! { + <> + { target_html } {" = "} + { render_expression(expr_id, script, format_params) } + { newline(format_params) } + + } +} + +fn render_match_block(match_cases: &[MatchCase], script: &MapperScript, format_params: &FormatParams) -> Html { + html! { + <> + {"!! TODO MATCH BLOCK !!!"} + {newline(format_params)} + + } +} + + +fn render_regex_source(source: &RegexSource) -> Html { + match source { + RegexSource::Identifier(ident) => render_identifier(ident), + RegexSource::Field(field) => render_field(field), + } +} + +fn render_regexp(field: &RegexSource, pattern: &String, regex: &Regex) -> Html { + html! { <> {render_regex_source(field)} {"~ '"}{ pattern } {"'"} } +} + +fn render_expression(expr_id: &ExprId, script: &MapperScript, format_params: &FormatParams) -> Html { + script.get_expr_by_id(*expr_id.deref()).map(|expression| { + match expression { + Expression::Identifier(ident) => render_identifier(ident), + Expression::StringLiteral(literal) => render_literal(literal), + Expression::NumberLiteral(num) => render_num_literal(num), + Expression::FieldAccess(field) => render_field(field), + Expression::VarAccess(name, field) => render_var_access(name, field), + Expression::RegexExpr { field, pattern, re_pattern } => render_regexp(field, pattern, re_pattern), + Expression::FunctionCall { name, args } => render_function_call(name, args, script, format_params), + Expression::Assignment { target, expr } => render_assignment(target, expr , script, format_params), + Expression::MatchBlock(match_cases) => render_match_block(match_cases, script, format_params), + Expression::MapBlock { key, cases} => render_map_block(key, cases, script, format_params), + Expression::NullValue => render_null_value(), + Expression::Block(expr_ids) => render_block(expr_ids, script, format_params), + } + }).unwrap_or_else(|| html! { {"ExprNotFound"} }) +} + +fn render_script(script: &MapperScript, pretty: bool, level: usize, do_indent: bool, p_count: usize) -> Html { + let format_params = FormatParams { + pretty, level, do_indent, p_count + }; + let items = script.statements.iter().map(|stmt| { + match stmt { + Statement::Expression(expr_id) => html! { + <> + {render_expression(expr_id, script, &format_params)} + {newline(&format_params)} + + }, + Statement::Comment(comment) => html!{
{ comment }
}, + } + }); + + html! { + <> + { for items } + + } +} diff --git a/webui/src/app/components/playlist/mappings.rs b/webui/src/app/components/playlist/mappings.rs index 68ac45c88..6c1d00628 100644 --- a/webui/src/app/components/playlist/mappings.rs +++ b/webui/src/app/components/playlist/mappings.rs @@ -1,7 +1,8 @@ use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::MappingDto; -use crate::app::components::{ConfigContext, NoContent}; +use shared::model::{MapperDto, MappingCounterDefinition, MappingDto}; +use crate::app::components::{ConfigContext, FilterView, MapperScriptView, NoContent}; +use crate::app::components::toggle_switch::ToggleSwitch; #[derive(Properties, PartialEq, Clone)] pub struct PlaylistMappingsProps { @@ -28,6 +29,25 @@ pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html { } }); + let render_mapper = |mapper: &MapperDto| { + html! { +
+ + + +
+ } + }; + + let render_counter = |mapper: &MappingCounterDefinition| { + html! { +
+ + +
+ } + }; + let render_mapping = |mapping: &MappingDto| { html! {
@@ -35,12 +55,26 @@ pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html { {mapping.id.clone()}
+
+ + +
+
+ { + for mapping.mapper.iter().flatten().map(|mapper| { + html! { render_mapper(mapper) } + }) + } +
+
+ { + for mapping.counter.iter().flatten().map(|counter| { + html! { render_counter(counter) } + }) + } +
} - // - // pub id: String, - // #[serde(default)] - // pub match_as_ascii: bool, // pub mapper: Option>, // pub counter: Option>, // #[serde(skip_serializing, skip_deserializing)] diff --git a/webui/src/app/components/playlist/mod.rs b/webui/src/app/components/playlist/mod.rs index e608f0ae9..e8ccab625 100644 --- a/webui/src/app/components/playlist/mod.rs +++ b/webui/src/app/components/playlist/mod.rs @@ -15,6 +15,7 @@ mod input; mod source_selector; mod playlist_explorer; mod playlist_update_view; +mod mapper_script_view; use std::rc::Rc; use yew_i18n::YewI18n; @@ -36,6 +37,7 @@ pub use self::source_selector::*; pub use self::playlist_editor_view::*; pub use self::playlist_explorer_view::*; pub use self::playlist_update_view::*; +pub use self::mapper_script_view::*; pub fn make_tags(data: &[(bool, &str)], translate: &YewI18n) -> Vec> { data.iter() diff --git a/webui/src/services/config_service.rs b/webui/src/services/config_service.rs index 819013adb..3d7ef878b 100644 --- a/webui/src/services/config_service.rs +++ b/webui/src/services/config_service.rs @@ -9,6 +9,7 @@ use log::error; use futures_signals::signal::Mutable; use futures_signals::signal::SignalExt; use shared::foundation::filter::{get_filter, prepare_templates}; +use shared::foundation::mapper::MapperScript; use shared::utils::concat_path_leading_slash; pub struct ConfigService { @@ -66,6 +67,20 @@ impl ConfigService { target.t_filter = get_filter(target.filter.as_str(), templates.as_ref()).ok(); } } + + if let Some(mappings) = app_config.mappings.as_mut() { + for mapping in mappings.mappings.mapping.iter_mut() { + let templates = mapping.templates.as_ref(); + if let Some(mappers) = mapping.mapper.as_mut() { + for mapper in mappers.iter_mut() { + mapper.t_filter = get_filter(mapper.filter.as_str(), templates).ok(); + mapper.t_script = MapperScript::parse(&mapper.script, templates).ok(); + } + } + } + + } + Some(Rc::new(app_config)) }, Err(err) => { From 30f0cce719eb29acaed588bdc291b217cf646db4 Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 7 Aug 2025 10:06:28 +0200 Subject: [PATCH 2/6] added web auth token ttl --- CHANGELOG.md | 1 + README.md | 1 + backend/src/auth/authenticator.rs | 7 ++++++- backend/src/model/config/web_auth.rs | 11 +++++++---- shared/src/model/config/web_auth.rs | 2 ++ 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb6f5ec4..36aa9e054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added hanging client connection release - Added `replace` built-in function for mapper scripts - New WebUI features +- Added `token_ttl_mins` to web_auth config to define auth token expiration duration. # 3.1.4 (2025-06-17) - share live stream refactored diff --git a/README.md b/README.md index 2a7f38e30..e6031b7b4 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,7 @@ log: - `enabled` can be deactivated if `enabled` is set to `false`. If not set default is `true`. - `issuer` - `secret` is used for jwt token generation. + - `token_ttl_mins` default 30 minutes, if set to 0 unlimited. - `userfile` is the file where the ui users are stored. If the filename is not absolute, `tuliprox` will look into the `config_dir`. If `userfile` is not given, the default value is `user.txt`. ```yaml web_ui: diff --git a/backend/src/auth/authenticator.rs b/backend/src/auth/authenticator.rs index 79312f528..8404751e8 100644 --- a/backend/src/auth/authenticator.rs +++ b/backend/src/auth/authenticator.rs @@ -32,7 +32,12 @@ fn create_jwt(web_auth_config: &WebAuthConfig, username: &str, roles: Vec 0 { + (now + Duration::minutes(duration as i64)).timestamp() + } else { + (now + Duration::days(365 * 100)).timestamp() // 100 years + }; let claims = Claims { username: username.to_string(), iss: web_auth_config.issuer.clone(), diff --git a/backend/src/model/config/web_auth.rs b/backend/src/model/config/web_auth.rs index 9ac884b66..6fb8083a9 100644 --- a/backend/src/model/config/web_auth.rs +++ b/backend/src/model/config/web_auth.rs @@ -11,6 +11,7 @@ pub struct WebAuthConfig { pub enabled: bool, pub issuer: String, pub secret: String, + pub token_ttl_mins: u32, pub userfile: Option, pub t_users: Option>, } @@ -20,8 +21,9 @@ impl From<&WebAuthConfigDto> for WebAuthConfig { fn from(dto: &WebAuthConfigDto) -> Self { Self { enabled: dto.enabled, - issuer: dto.issuer.to_string(), - secret: dto.secret.to_string(), + issuer: dto.issuer.clone(), + secret: dto.secret.clone(), + token_ttl_mins: dto.token_ttl_mins, userfile: dto.userfile.clone(), t_users: None, } @@ -32,8 +34,9 @@ impl From<&WebAuthConfig> for WebAuthConfigDto { fn from(instance: &WebAuthConfig) -> Self { Self { enabled: instance.enabled, - issuer: instance.issuer.to_string(), - secret: instance.secret.to_string(), + issuer: instance.issuer.clone(), + secret: instance.secret.clone(), + token_ttl_mins: instance.token_ttl_mins, userfile: instance.userfile.clone(), } } diff --git a/shared/src/model/config/web_auth.rs b/shared/src/model/config/web_auth.rs index db1513535..b9786b5b7 100644 --- a/shared/src/model/config/web_auth.rs +++ b/shared/src/model/config/web_auth.rs @@ -7,6 +7,8 @@ pub struct WebAuthConfigDto { pub enabled: bool, pub issuer: String, pub secret: String, + #[serde(default)] + pub token_ttl_mins: u32, #[serde(default, skip_serializing_if = "Option::is_none")] pub userfile: Option, } From c0d2cd044852ae006d6b241d7057620fd70c82c2 Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 7 Aug 2025 14:59:26 +0200 Subject: [PATCH 3/6] Batch provider connection size reporting fixed --- .../src/api/model/active_provider_manager.rs | 108 ++++++++++-------- backend/src/api/model/provider_config.rs | 28 +++-- backend/src/auth/authenticator.rs | 2 +- shared/src/foundation/mapper.rs | 4 +- 4 files changed, 79 insertions(+), 63 deletions(-) diff --git a/backend/src/api/model/active_provider_manager.rs b/backend/src/api/model/active_provider_manager.rs index 187fa3a23..2c2a59896 100644 --- a/backend/src/api/model/active_provider_manager.rs +++ b/backend/src/api/model/active_provider_manager.rs @@ -1,4 +1,4 @@ -use crate::api::model::{ProviderConfig, ProviderConfigConnection, ProviderConfigWrapper, ProviderConnectionChangeSender}; +use crate::api::model::{ProviderConfig, ProviderConfigConnection, ProviderConfigWrapper, ProviderConnectionChangeCallback, ProviderConnectionChangeSender}; use crate::model::{AppConfig, ConfigInput}; use arc_swap::ArcSwap; use log::{debug, log_enabled, trace}; @@ -194,12 +194,12 @@ struct SingleProviderLineup { } impl SingleProviderLineup { - fn new<'a, F>(cfg: &ConfigInput, get_connection: Option, connection_change_sender: ProviderConnectionChangeSender) -> Self + fn new<'a, F>(cfg: &ConfigInput, get_connection: Option, connection_change: &ProviderConnectionChangeCallback) -> Self where F: Fn(&str) -> Option<&'a ProviderConfigConnection>, { Self { - provider: ProviderConfigWrapper::new(ProviderConfig::new(cfg, get_connection, connection_change_sender)), + provider: ProviderConfigWrapper::new(ProviderConfig::new(cfg, get_connection, Arc::clone(connection_change))), } } @@ -264,19 +264,20 @@ impl ProviderPriorityGroup { #[repr(align(64))] #[derive(Debug)] struct MultiProviderLineup { + name: String, providers: Vec, index: AtomicUsize, } impl MultiProviderLineup { - pub fn new<'a, F>(input: &ConfigInput, get_connection: Option, connection_change_sender: &ProviderConnectionChangeSender) -> Self + pub fn new<'a, F>(cfg_input: &ConfigInput, get_connection: Option, connection_change: &ProviderConnectionChangeCallback) -> Self where F: Fn(&str) -> Option<&'a ProviderConfigConnection> + Copy, { - let mut inputs = vec![ProviderConfigWrapper::new(ProviderConfig::new(input, get_connection, connection_change_sender.clone()))]; - if let Some(aliases) = &input.aliases { + let mut inputs = vec![ProviderConfigWrapper::new(ProviderConfig::new(cfg_input, get_connection, Arc::clone(connection_change)))]; + if let Some(aliases) = &cfg_input.aliases { for alias in aliases { - inputs.push(ProviderConfigWrapper::new(ProviderConfig::new_alias(input, alias, get_connection, connection_change_sender.clone()))); + inputs.push(ProviderConfigWrapper::new(ProviderConfig::new_alias(cfg_input, alias, get_connection, Arc::clone(connection_change)))); } } let mut providers = HashMap::new(); @@ -297,6 +298,7 @@ impl MultiProviderLineup { }).collect(); Self { + name: cfg_input.name.clone(), providers, index: AtomicUsize::new(0), } @@ -484,6 +486,24 @@ impl MultiProviderLineup { } } } + + pub async fn get_total_connections(&self) -> usize { + let mut total_connections = 0; + + for group in &self.providers { + match group { + ProviderPriorityGroup::SingleProviderGroup(provider) => { + total_connections += provider.get_current_connections().await; + }, + ProviderPriorityGroup::MultiProviderGroup(_, providers) => { + for provider in providers { + total_connections += provider.get_current_connections().await; + } + } + } + } + total_connections + } } @@ -507,13 +527,24 @@ impl ProviderLineupManager { } } - fn create_lineup(input: &ConfigInput, provider_connections: Option<&HashMap<&str, ProviderConfigConnection>>, connection_change_sender: ProviderConnectionChangeSender) -> ProviderLineup { + fn create_lineup(cfg_input: &ConfigInput, provider_connections: Option<&HashMap<&str, ProviderConfigConnection>>, connection_change_sender: ProviderConnectionChangeSender) -> ProviderLineup { let get_connections = provider_connections.map(|c| |name: &str| c.get(name)); - if input.aliases.as_ref().is_some_and(|a| !a.is_empty()) { - ProviderLineup::Multi(MultiProviderLineup::new(input, get_connections, &connection_change_sender)) + let cfg_name = cfg_input.name.clone(); + let on_connection_change: ProviderConnectionChangeCallback = Arc::new(move |_name: &str, connections: usize| { + let connection_change_sender = connection_change_sender.clone(); + let cfg_name = cfg_name.clone(); + tokio::spawn(async move { + let _ = connection_change_sender.send((cfg_name, connections)).await; + }); + }); + + let on_connection_change = Arc::new(on_connection_change); + + if cfg_input.aliases.as_ref().is_some_and(|a| !a.is_empty()) { + ProviderLineup::Multi(MultiProviderLineup::new(cfg_input, get_connections, &on_connection_change)) } else { - ProviderLineup::Single(SingleProviderLineup::new(input, get_connections, connection_change_sender)) + ProviderLineup::Single(SingleProviderLineup::new(cfg_input, get_connections, &on_connection_change)) } } @@ -711,47 +742,21 @@ impl ProviderLineupManager { pub async fn active_connections(&self) -> Option> { let mut result = HashMap::::new(); - let mut add_provider = async |provider: Option<&ProviderConfig>, name: Option, count: usize| { - if let Some(provider_cfg) = provider { - let count = provider_cfg.get_current_connections().await; - if count > 0 { - result.insert(provider_cfg.name.to_string(), count); - } - } else if count > 0 { - result.insert(name.unwrap_or_default(), count); + let mut add_provider = |name: String, count: usize| { + if count > 0 { + result.insert(name, count); } }; let providers = self.providers.load(); for lineup in providers.iter() { match lineup { ProviderLineup::Single(provider_lineup) => { - add_provider(Some(&provider_lineup.provider), None, 0).await; + let connections = provider_lineup.provider.get_current_connections().await; + add_provider(provider_lineup.provider.name.clone(), connections); } ProviderLineup::Multi(provider_lineup) => { - for provider_group in &provider_lineup.providers { - match provider_group { - ProviderPriorityGroup::SingleProviderGroup(provider) => { - add_provider(Some(provider), None, 0).await; - } - ProviderPriorityGroup::MultiProviderGroup(_, providers) => { - let mut connections = 0; - let mut name = None; - for provider in providers { - if name.is_none() { - name = Some(format!("[{}]", provider.name.clone())); - } - connections += provider.get_current_connections().await; - } - if connections > 0 { - add_provider(None, name, connections).await; - } - - // for provider in providers { - // add_provider(provider).await; - // } - } - } - } + let connections = provider_lineup.get_total_connections().await; + add_provider(provider_lineup.name.clone(), connections); } } } @@ -1042,9 +1047,10 @@ mod tests { #[test] fn test_acquire_when_capacity_available() { let cfg = create_config_input(1, "provider5_1", 1, 2); - let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1); + let on_connection_change = move |_name, _connections| {}; + let change_tx = Arc::new(on_connection_change); let dummy_get_connection = |_s: &str| -> Option<&ProviderConfigConnection> { None }; - let lineup = SingleProviderLineup::new(&cfg, Some(dummy_get_connection), change_tx); + let lineup = SingleProviderLineup::new(&cfg, Some(dummy_get_connection), &change_tx); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { // First acquire attempt should succeed @@ -1063,10 +1069,11 @@ mod tests { #[test] fn test_release_connection() { let cfg = create_config_input(1, "provider7_1", 1, 2); - let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1); + let on_connection_change = move |_name, _connections| {}; + let change_tx = Arc::new(on_connection_change); let dummy_get_connection = |_s: &str| -> Option<&ProviderConfigConnection> { None }; - let lineup = SingleProviderLineup::new(&cfg, Some(dummy_get_connection), change_tx); + let lineup = SingleProviderLineup::new(&cfg, Some(dummy_get_connection), &change_tx); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { // Acquire two connections @@ -1127,9 +1134,10 @@ mod tests { #[test] fn test_concurrent_acquire() { let cfg = create_config_input(1, "provider9_1", 1, 2); - let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1); + let on_connection_change = move |_name, _connections| {}; + let change_tx = Arc::new(on_connection_change); let dummy_get_connection = |_s: &str| -> Option<&ProviderConfigConnection> { None }; - let lineup = Arc::new(SingleProviderLineup::new(&cfg, Some(dummy_get_connection), change_tx)); + let lineup = Arc::new(SingleProviderLineup::new(&cfg, Some(dummy_get_connection), &change_tx)); let available_count = Arc::new(AtomicU16::new(2)); let grace_period_count = Arc::new(AtomicU16::new(1)); diff --git a/backend/src/api/model/provider_config.rs b/backend/src/api/model/provider_config.rs index 688130296..83cc06f89 100644 --- a/backend/src/api/model/provider_config.rs +++ b/backend/src/api/model/provider_config.rs @@ -12,6 +12,8 @@ use shared::write_if_some; pub type ProviderConnectionChangeSender = tokio::sync::mpsc::Sender<(String, usize)>; pub type ProviderConnectionChangeReceiver = tokio::sync::mpsc::Receiver<(String, usize)>; +pub type ProviderConnectionChangeCallback = Arc; + #[derive(Debug, Clone, Copy)] pub enum ProviderConfigAllocation { Exhausted, @@ -33,7 +35,6 @@ pub struct ProviderConfigConnection { /// `max_connections`: Maximum allowed concurrent connections. /// `priority`: Priority level for selecting providers. /// `current_connections`: A `RwLock` to safely track the number of active connections. -#[derive(Debug)] pub struct ProviderConfig { pub id: u16, pub name: String, @@ -44,7 +45,8 @@ pub struct ProviderConfig { max_connections: usize, priority: i16, connection: RwLock, - connection_change_tx: ProviderConnectionChangeSender, + //connection_change_tx: ProviderConnectionChangeSender, + on_connection_change: ProviderConnectionChangeCallback, } impl fmt::Display for ProviderConfig { @@ -65,6 +67,12 @@ impl fmt::Display for ProviderConfig { } } +impl fmt::Debug for ProviderConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self}") + } +} + impl PartialEq for ProviderConfig { fn eq(&self, other: &Self) -> bool { self.id == other.id @@ -82,16 +90,16 @@ impl PartialEq for ProviderConfig { macro_rules! modify_connections { ($self:ident, $guard:ident, +1) => {{ $guard.current_connections += 1; - $self.notify_connection_change($guard.current_connections).await; + $self.notify_connection_change($guard.current_connections); }}; ($self:ident, $guard:ident, -1) => {{ $guard.current_connections -= 1; - $self.notify_connection_change($guard.current_connections).await; + $self.notify_connection_change($guard.current_connections); }}; } impl ProviderConfig { - pub fn new<'a, F>(cfg: &ConfigInput, get_connection: Option, connection_change_tx: ProviderConnectionChangeSender) -> Self + pub fn new<'a, F>(cfg: &ConfigInput, get_connection: Option, on_connection_change: ProviderConnectionChangeCallback) -> Self where F: Fn(&str) -> Option<&'a ProviderConfigConnection>, { @@ -105,11 +113,11 @@ impl ProviderConfig { max_connections: cfg.max_connections as usize, priority: cfg.priority, connection: RwLock::new(get_connection.and_then(|f| f(cfg.name.as_str())).map_or_else(Default::default, Clone::clone)), - connection_change_tx + on_connection_change } } - pub fn new_alias<'a, F>(cfg: &ConfigInput, alias: &ConfigInputAlias, get_connection: Option, connection_change_tx: ProviderConnectionChangeSender) -> Self + pub fn new_alias<'a, F>(cfg: &ConfigInput, alias: &ConfigInputAlias, get_connection: Option, on_connection_change: ProviderConnectionChangeCallback) -> Self where F: Fn(&str) -> Option<&'a ProviderConfigConnection>, { @@ -123,7 +131,7 @@ impl ProviderConfig { max_connections: alias.max_connections as usize, priority: alias.priority, connection: RwLock::new(get_connection.and_then(|f| f(alias.name.as_str())).map_or_else(Default::default, Clone::clone)), - connection_change_tx, + on_connection_change, } } @@ -131,8 +139,8 @@ impl ProviderConfig { InputUserInfo::new(self.input_type, self.username.as_deref(), self.password.as_deref(), &self.url) } - async fn notify_connection_change(&self, new_connections: usize) { - let _ = self.connection_change_tx.send((self.name.clone(), new_connections)).await; + fn notify_connection_change(&self, new_connections: usize) { + (self.on_connection_change)(&self.name, new_connections); } #[inline] diff --git a/backend/src/auth/authenticator.rs b/backend/src/auth/authenticator.rs index 8404751e8..d710dff3b 100644 --- a/backend/src/auth/authenticator.rs +++ b/backend/src/auth/authenticator.rs @@ -34,7 +34,7 @@ fn create_jwt(web_auth_config: &WebAuthConfig, username: &str, roles: Vec 0 { - (now + Duration::minutes(duration as i64)).timestamp() + (now + Duration::minutes(i64::from(duration))).timestamp() } else { (now + Duration::days(365 * 100)).timestamp() // 100 years }; diff --git a/shared/src/foundation/mapper.rs b/shared/src/foundation/mapper.rs index 3912574f1..82332db66 100644 --- a/shared/src/foundation/mapper.rs +++ b/shared/src/foundation/mapper.rs @@ -135,7 +135,7 @@ impl FromStr for BuiltInFunction { "first" => Ok(Self::First), "template" => Ok(Self::Template), "replace" => Ok(Self::Replace), - _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown function {}", s), + _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown function {s}"), } } } @@ -154,7 +154,7 @@ impl Display for BuiltInFunction { Self::Template => "template", Self::Replace => "replace", }.to_owned(); - write!(f, "{}", str) + write!(f, "{str}") } } From e7e27adc8af3754c3458af99cc83e043ddcd56ec Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 7 Aug 2025 19:46:49 +0200 Subject: [PATCH 4/6] react frontend deleted --- .dockerignore | 2 +- Cargo.toml | 2 +- bin/build_aarch64.sh | 2 +- bin/build_armv7.sh | 2 +- bin/build_darwin.sh | 2 +- bin/build_fe.sh | 2 +- bin/build_github_docker.sh | 2 +- bin/build_github_docker_aarch64.sh | 2 +- bin/build_github_docker_beta.sh | 2 +- bin/build_lin.sh | 2 +- bin/build_lin_static.sh | 2 +- bin/build_win.sh | 2 +- bin/release.sh | 2 +- docker/Dockerfile | 8 +- {webui => frontend}/Cargo.toml | 0 {webui => frontend}/Trunk.toml | 0 frontend/eslintrc.json | 54 - {webui => frontend}/index.html | 0 frontend/package.json | 83 -- frontend/public/assets/fonts.css | 2 +- frontend/public/assets/i18n/de_common.json | 123 -- .../public/assets/i18n/en.json | 0 frontend/public/assets/i18n/en_common.json | 206 --- frontend/public/assets/i18n/es_common.json | 112 -- frontend/public/assets/i18n/fr_common.json | 112 -- frontend/public/assets/i18n/it_common.json | 112 -- frontend/public/assets/i18n/ru_common.json | 112 -- frontend/public/assets/i18n/tr_common.json | 115 -- {webui => frontend}/public/assets/icons.json | 0 frontend/public/config.json | 11 +- frontend/public/index.html | 28 - frontend/public/manifest.json | 15 - frontend/public/robots.txt | 3 - {webui => frontend}/scss/_default.scss | 0 {webui => frontend}/scss/_size.scss | 0 {webui => frontend}/scss/_theme.scss | 0 {webui => frontend}/scss/app/_app.scss | 0 {webui => frontend}/scss/app/_app_header.scss | 0 {webui => frontend}/scss/app/_component.scss | 0 .../scss/app/components/_breadcrumbs.scss | 0 .../scss/app/components/_card.scss | 0 .../scss/app/components/_chip.scss | 0 .../scss/app/components/_collapse_panel.scss | 0 .../scss/app/components/_custom_dialog.scss | 1 + .../scss/app/components/_dialog.scss | 0 .../components/_drop_down_icon_button.scss | 0 .../scss/app/components/_filter.scss | 0 .../scss/app/components/_hide_content.scss | 0 .../scss/app/components/_icon_button.scss | 0 .../scss/app/components/_input.scss | 0 .../scss/app/components/_list_view.scss | 0 .../app/components/_loading_indicator.scss | 0 .../scss/app/components/_login.scss | 0 .../scss/app/components/_mapper_script.scss | 4 + .../scss/app/components/_menu_item.scss | 0 .../scss/app/components/_no_content.scss | 0 .../scss/app/components/_panel.scss | 0 .../scss/app/components/_popup_menu.scss | 0 .../scss/app/components/_preferences.scss | 0 .../app/components/_radio_button_group.scss | 0 .../scss/app/components/_reveal_content.scss | 0 .../scss/app/components/_search.scss | 0 .../scss/app/components/_sidebar.scss | 0 .../scss/app/components/_table.scss | 0 .../scss/app/components/_tag_list.scss | 0 .../scss/app/components/_text_button.scss | 0 .../scss/app/components/_toastr.scss | 0 .../scss/app/components/_toggle_switch.scss | 0 .../components/dashboard/_action_card.scss | 0 .../components/dashboard/_dashboard_view.scss | 0 .../app/components/dashboard/_stats_view.scss | 0 .../components/dashboard/_status_card.scss | 0 .../playlist/_playlist_editor_view.scss | 0 .../playlist/_playlist_explorer.scss | 0 .../playlist/_playlist_explorer_view.scss | 0 .../playlist/_playlist_mappings.scss | 59 + .../playlist/_playlist_processing.scss | 0 .../playlist/_playlist_update_view.scss | 3 +- .../components/playlist/_source_selector.scss | 0 .../assistant/_playlist_assistant.scss | 0 .../playlist/input/_input_table.scss | 0 .../playlist/input/_input_type.scss | 0 .../playlist/target/_hdhomerun_output.scss | 0 .../playlist/target/_m3u_output.scss | 0 .../playlist/target/_strm_output.scss | 0 .../playlist/target/_target_common.scss | 0 .../playlist/target/_target_options.scss | 0 .../playlist/target/_target_output.scss | 0 .../playlist/target/_target_rename.scss | 0 .../playlist/target/_target_sort.scss | 0 .../playlist/target/_target_table.scss | 0 .../playlist/target/_target_watch.scss | 0 .../playlist/target/_xtream_output.scss | 0 .../components/userlist/_max_connections.scss | 0 .../app/components/userlist/_proxy_type.scss | 0 .../app/components/userlist/_user_state.scss | 0 .../components/userlist/_userlist_view.scss | 0 {webui => frontend}/scss/main.scss | 0 frontend/src/api/api-service.ts | 153 --- frontend/src/api/auth-api-service.ts | 35 - frontend/src/api/config-api-service.ts | 37 - frontend/src/api/file-api-service.ts | 43 - frontend/src/api/playlist-api-service.ts | 35 - frontend/src/api/server-status-api-service.ts | 23 - frontend/src/api/user-config-api-service.ts | 26 - frontend/src/app-user/app-user.scss | 73 -- frontend/src/app-user/app-user.tsx | 49 - frontend/src/app/app.scss | 76 -- frontend/src/app/app.tsx | 90 -- .../src/app/components/authentication.rs | 0 .../src/app/components/breadcrumbs.rs | 0 .../src/app/components/card.rs | 0 .../src/app/components/chip.rs | 0 .../src/app/components/collapse_panel.rs | 0 .../src/app/components/confirm_dialog.rs | 0 .../src/app/components/content_dialog.rs | 0 .../src/app/components/custom_dialog.rs | 0 .../app/components/dashboard/action_card.rs | 0 .../components/dashboard/dashboard_view.rs | 0 .../dashboard/discord_action_card.rs | 0 .../dashboard/documentation_action_card.rs | 0 .../dashboard/github_action_card.rs | 0 .../dashboard/ipinfo_action_card.rs | 0 .../src/app/components/dashboard/mod.rs | 0 .../playlist_progress_status_card.rs | 0 .../app/components/dashboard/stats_view.rs | 0 .../app/components/dashboard/status_card.rs | 0 .../components/dashboard/user_action_card.rs | 0 .../dashboard/version_action_card.rs | 0 .../app/components/drop_down_icon_button.rs | 0 .../src/app/components/hide_content.rs | 0 .../src/app/components/home.rs | 0 .../src/app/components/icon_button.rs | 0 .../src/app/components/input.rs | 0 .../src/app/components/loading_indicator.rs | 0 .../src/app/components/login.rs | 0 .../src/app/components/menu_item.rs | 0 {webui => frontend}/src/app/components/mod.rs | 0 .../src/app/components/no_content.rs | 0 .../src/app/components/panel.rs | 0 .../app/components/playlist/assistant/mod.rs | 0 .../components/playlist/assistant/playlist.rs | 0 .../assistant/playlist_assistant_context.rs | 0 .../playlist/assistant/step_name.rs | 0 .../playlist/assistant/step_schedule.rs | 0 .../playlist/assistant/step_type.rs | 0 .../src/app/components/playlist/create.rs | 0 .../app/components/playlist/filter_view.rs | 0 .../playlist/input/input_headers.rs | 0 .../playlist/input/input_options.rs | 0 .../playlist/input/input_type_view.rs | 0 .../src/app/components/playlist/input/mod.rs | 0 .../app/components/playlist/input_table.rs | 0 .../src/app/components/playlist/list.rs | 0 .../components/playlist/mapper_script_view.rs | 48 +- .../src/app/components/playlist/mappings.rs | 7 +- .../src/app/components/playlist/mod.rs | 0 .../playlist/playlist_editor_page.rs | 0 .../playlist/playlist_editor_view.rs | 0 .../components/playlist/playlist_explorer.rs | 0 .../playlist/playlist_explorer_page.rs | 0 .../playlist/playlist_explorer_view.rs | 0 .../playlist/playlist_update_view.rs | 0 .../src/app/components/playlist/processing.rs | 0 .../components/playlist/source_selector.rs | 0 .../playlist/target/hdhomerun_output.rs | 0 .../components/playlist/target/m3u_output.rs | 0 .../src/app/components/playlist/target/mod.rs | 0 .../components/playlist/target/strm_output.rs | 0 .../playlist/target/target_options.rs | 0 .../playlist/target/target_output.rs | 0 .../playlist/target/target_rename.rs | 0 .../components/playlist/target/target_sort.rs | 0 .../playlist/target/target_watch.rs | 0 .../playlist/target/xtream_output.rs | 0 .../app/components/playlist/target_table.rs | 0 .../src/app/components/popup_menu.rs | 0 .../src/app/components/radio_button_group.rs | 0 .../src/app/components/reveal_content.rs | 0 .../src/app/components/search.rs | 0 .../src/app/components/sidebar.rs | 0 .../src/app/components/svg_icon.rs | 0 .../src/app/components/table.rs | 0 .../src/app/components/tag_list.rs | 0 .../src/app/components/text_button.rs | 0 .../src/app/components/toastr.rs | 0 .../src/app/components/toggle_switch.rs | 0 .../src/app/components/userlist/edit.rs | 0 .../src/app/components/userlist/list.rs | 0 .../components/userlist/max_connections.rs | 0 .../src/app/components/userlist/mod.rs | 0 .../src/app/components/userlist/page.rs | 0 .../components/userlist/proxy_type_view.rs | 0 .../app/components/userlist/user_status.rs | 0 .../src/app/components/userlist/user_table.rs | 0 .../app/components/userlist/userlist_view.rs | 0 {webui => frontend}/src/app/context.rs | 0 {webui => frontend}/src/app/mod.rs | 0 .../api-proxy-view/api-proxy-view.scss | 36 - .../api-proxy-view/api-proxy-view.tsx | 63 - .../authentication/authentication.tsx | 49 - frontend/src/component/checkbox/checkbox.scss | 60 - frontend/src/component/checkbox/checkbox.tsx | 41 - .../clipboard-viewer/clipboard-viewer.scss | 43 - .../clipboard-viewer/clipboard-viewer.tsx | 52 - .../component/date-picker/date-picker.scss | 75 -- .../src/component/date-picker/date-picker.tsx | 66 - .../file-download/file-download.scss | 76 -- .../component/file-download/file-download.tsx | 118 -- .../src/component/form-view/form-view.scss | 88 -- .../src/component/form-view/from-view.tsx | 172 --- .../component/input-field/input-field.scss | 68 - .../src/component/input-field/input-field.tsx | 18 - .../loading-indicator/loading-indicator.scss | 37 - .../loading-indicator/loading-indicator.tsx | 15 - frontend/src/component/login/login.scss | 78 -- frontend/src/component/login/login.tsx | 62 - .../main-config-view/main-config-view.scss | 69 - .../main-config-view/main-config-view.tsx | 455 ------- .../src/component/map-editor/map-editor.scss | 72 - .../src/component/map-editor/map-editor.tsx | 83 -- frontend/src/component/panel/panel.scss | 9 - frontend/src/component/panel/panel.tsx | 13 - .../playlist-browser/playlist-browser.scss | 30 - .../playlist-browser/playlist-browser.tsx | 137 -- .../playlist-filter/playlist-filter.scss | 30 - .../playlist-filter/playlist-filter.tsx | 74 -- .../playlist-gallery/playlist-gallery.scss | 222 ---- .../playlist-gallery/playlist-gallery.tsx | 371 ------ .../playlist-tree/playlist-tree.scss | 100 -- .../component/playlist-tree/playlist-tree.tsx | 203 --- .../playlist-video/channel-view.scss | 206 --- .../component/playlist-video/channel-view.tsx | 154 --- .../playlist-video/playlist-video.tsx | 25 - .../playlist-viewer/playlist-viewer.scss | 39 - .../playlist-viewer/playlist-viewer.tsx | 202 --- .../src/component/popup-menu/popup-menu.scss | 14 - .../src/component/popup-menu/popup-menu.tsx | 34 - .../component/preferences/preferences.scss | 102 -- .../src/component/preferences/preferences.tsx | 74 -- frontend/src/component/progress/progress.scss | 59 - frontend/src/component/progress/progress.tsx | 17 - .../component/proxy-select/proxy-select.scss | 52 - .../component/proxy-select/proxy-select.tsx | 80 -- .../schedule-editor/schedule-editor.scss | 37 - .../schedule-editor/schedule-editor.tsx | 83 -- .../component/setup-wizard/_setup-common.scss | 45 - .../progress-bar/progress-bar.scss | 3 - .../progress-bar/progress-bar.tsx | 16 - .../component/setup-wizard/setup-wizard.scss | 0 .../component/setup-wizard/setup-wizard.tsx | 27 - .../step-summary/step-summary.scss | 5 - .../step-summary/step-summary.tsx | 23 - .../step-welcome/step-welcome.scss | 9 - .../step-welcome/step-welcome.tsx | 22 - frontend/src/component/sidebar/sidebar.scss | 34 - frontend/src/component/sidebar/sidebar.tsx | 27 - .../source-selector-editor.scss | 37 - .../source-selector-editor.tsx | 158 --- .../source-selector/source-selector.scss | 10 - .../source-selector/source-selector.tsx | 73 -- .../component/split-panel/split-panel.scss | 30 - .../src/component/split-panel/split-panel.tsx | 44 - .../component/status-view/status-view.scss | 38 - .../src/component/status-view/status-view.tsx | 70 - frontend/src/component/svg-icon/svg-icon.scss | 11 - frontend/src/component/svg-icon/svg-icon.tsx | 16 - frontend/src/component/tab-set/tab-set.scss | 44 - frontend/src/component/tab-set/tab-set.tsx | 63 - .../src/component/tag-input/tag-input.scss | 12 - .../src/component/tag-input/tag-input.tsx | 29 - .../src/component/tag-select/tag-select.scss | 24 - .../src/component/tag-select/tags-select.tsx | 67 - .../target-update-view.scss | 20 - .../target-update-view/target-update-view.tsx | 48 - frontend/src/component/tooltip/tooltip.scss | 14 - frontend/src/component/tooltip/tooltip.tsx | 61 - .../component/user-editor/user-editor.scss | 23 - .../src/component/user-editor/user-editor.tsx | 88 -- .../user-playlist/user-playlist.scss | 32 - .../component/user-playlist/user-playlist.tsx | 140 -- .../user-target-playlist.scss | 91 -- .../user-target-playlist.tsx | 162 --- .../src/component/user-view/user-view.scss | 280 ---- .../src/component/user-view/user-view.tsx | 504 ------- .../component/video-player/video-player.scss | 13 - .../component/video-player/video-player.tsx | 194 --- frontend/src/config.ts | 21 - {webui => frontend}/src/error.rs | 0 frontend/src/hook/use-const.ts | 15 - frontend/src/hook/use-translator.ts | 20 - {webui => frontend}/src/hooks/mod.rs | 0 .../src/hooks/use_icon_context.rs | 0 .../src/hooks/use_server_status.rs | 0 .../src/hooks/use_service_context.rs | 0 frontend/src/icons/icons.tsx | 73 -- frontend/src/index.scss | 52 - frontend/src/index.tsx | 57 - {webui => frontend}/src/lib.rs | 0 {webui => frontend}/src/main.rs | 0 {webui => frontend}/src/model/busy_status.rs | 0 {webui => frontend}/src/model/dialog.rs | 0 .../src/model/event_message.rs | 0 .../src/model/explorer_source_type.rs | 0 frontend/src/model/file-download.ts | 20 - frontend/src/model/message-kind.ts | 6 - {webui => frontend}/src/model/mod.rs | 0 frontend/src/model/playlist-request.ts | 15 - frontend/src/model/playlist.ts | 81 -- frontend/src/model/server-config.ts | 285 ---- frontend/src/model/server-status.tsx | 16 - frontend/src/model/tree-selection.ts | 5 - frontend/src/model/ui-config.ts | 19 - {webui => frontend}/src/model/view_type.rs | 0 {webui => frontend}/src/model/web_config.rs | 0 .../src/provider/dialog_provider.rs | 0 .../src/provider/icon_context_provider.rs | 0 {webui => frontend}/src/provider/mod.rs | 0 frontend/src/provider/service-provider.tsx | 13 - .../src/provider/service_context_provider.rs | 0 frontend/src/react-app-env.d.ts | 1 - frontend/src/scss/_common.scss | 134 -- frontend/src/scss/_default.scss | 123 -- frontend/src/scss/_panel.scss | 6 - frontend/src/scss/_preferences.scss | 53 - frontend/src/scss/_theme.scss | 110 -- frontend/src/service/auth-service.ts | 123 -- frontend/src/service/config-service.ts | 38 - frontend/src/service/file-service.ts | 27 - frontend/src/service/playlist-service.ts | 62 - frontend/src/service/server-status-service.ts | 16 - frontend/src/service/service-context.ts | 58 - frontend/src/service/user-config-service.ts | 19 - .../src/services/auth_service.rs | 0 .../src/services/config_service.rs | 0 .../src/services/dialog_service.rs | 0 .../src/services/event_service.rs | 0 {webui => frontend}/src/services/mod.rs | 0 .../src/services/playlist_service.rs | 0 {webui => frontend}/src/services/requests.rs | 0 .../src/services/status_service.rs | 0 .../src/services/toastr_service.rs | 0 .../src/services/websocket_service.rs | 0 frontend/src/utils/click-away-listener.tsx | 130 -- frontend/src/utils/clipboard.ts | 51 - frontend/src/utils/config-utils.ts | 13 - frontend/src/utils/date-utils.ts | 52 - frontend/src/utils/fetcher.ts | 19 - frontend/src/utils/i18n.ts | 40 - frontend/src/utils/icon-utils.tsx | 6 - {webui => frontend}/src/utils/mod.rs | 0 {webui => frontend}/src/utils/storage.rs | 0 frontend/src/utils/text-generator.ts | 1162 ----------------- frontend/src/utils/uuid.ts | 17 - frontend/tsconfig.json | 27 - webui/public/assets/favicon.ico | Bin 15406 -> 0 bytes webui/public/assets/fonts.css | 59 - .../assets/fonts/rubik-arabic-400-italic.woff | Bin 22396 -> 0 bytes .../fonts/rubik-arabic-400-italic.woff2 | Bin 17368 -> 0 bytes .../assets/fonts/rubik-arabic-400-normal.woff | Bin 22364 -> 0 bytes .../fonts/rubik-arabic-400-normal.woff2 | Bin 17348 -> 0 bytes .../fonts/rubik-cyrillic-400-italic.woff | Bin 10700 -> 0 bytes .../fonts/rubik-cyrillic-400-italic.woff2 | Bin 8480 -> 0 bytes .../fonts/rubik-cyrillic-400-normal.woff | Bin 9928 -> 0 bytes .../fonts/rubik-cyrillic-400-normal.woff2 | Bin 7852 -> 0 bytes .../fonts/rubik-cyrillic-ext-400-italic.woff | Bin 9004 -> 0 bytes .../fonts/rubik-cyrillic-ext-400-italic.woff2 | Bin 6880 -> 0 bytes .../fonts/rubik-cyrillic-ext-400-normal.woff | Bin 8208 -> 0 bytes .../fonts/rubik-cyrillic-ext-400-normal.woff2 | Bin 6252 -> 0 bytes .../assets/fonts/rubik-hebrew-400-italic.woff | Bin 6404 -> 0 bytes .../fonts/rubik-hebrew-400-italic.woff2 | Bin 4996 -> 0 bytes .../assets/fonts/rubik-hebrew-400-normal.woff | Bin 6352 -> 0 bytes .../fonts/rubik-hebrew-400-normal.woff2 | Bin 4876 -> 0 bytes .../assets/fonts/rubik-latin-400-italic.woff | Bin 24212 -> 0 bytes .../assets/fonts/rubik-latin-400-italic.woff2 | Bin 19196 -> 0 bytes .../assets/fonts/rubik-latin-400-normal.woff | Bin 24000 -> 0 bytes .../assets/fonts/rubik-latin-400-normal.woff2 | Bin 18856 -> 0 bytes .../fonts/rubik-latin-ext-400-italic.woff | Bin 14684 -> 0 bytes .../fonts/rubik-latin-ext-400-italic.woff2 | Bin 11304 -> 0 bytes .../fonts/rubik-latin-ext-400-normal.woff | Bin 13668 -> 0 bytes .../fonts/rubik-latin-ext-400-normal.woff2 | Bin 10584 -> 0 bytes webui/public/assets/missing-logo.svg | 22 - webui/public/assets/placeholder.png | Bin 6049 -> 0 bytes webui/public/assets/schedule.svg | 204 --- webui/public/assets/tuliprox-logo.svg | 36 - webui/public/config.json | 12 - .../playlist/_playlist_mappings.scss | 42 - 387 files changed, 125 insertions(+), 11989 deletions(-) rename {webui => frontend}/Cargo.toml (100%) rename {webui => frontend}/Trunk.toml (100%) delete mode 100644 frontend/eslintrc.json rename {webui => frontend}/index.html (100%) delete mode 100755 frontend/package.json delete mode 100644 frontend/public/assets/i18n/de_common.json rename {webui => frontend}/public/assets/i18n/en.json (100%) delete mode 100644 frontend/public/assets/i18n/en_common.json delete mode 100644 frontend/public/assets/i18n/es_common.json delete mode 100644 frontend/public/assets/i18n/fr_common.json delete mode 100644 frontend/public/assets/i18n/it_common.json delete mode 100644 frontend/public/assets/i18n/ru_common.json delete mode 100644 frontend/public/assets/i18n/tr_common.json rename {webui => frontend}/public/assets/icons.json (100%) delete mode 100644 frontend/public/index.html delete mode 100644 frontend/public/manifest.json delete mode 100644 frontend/public/robots.txt rename {webui => frontend}/scss/_default.scss (100%) rename {webui => frontend}/scss/_size.scss (100%) rename {webui => frontend}/scss/_theme.scss (100%) rename {webui => frontend}/scss/app/_app.scss (100%) rename {webui => frontend}/scss/app/_app_header.scss (100%) rename {webui => frontend}/scss/app/_component.scss (100%) rename {webui => frontend}/scss/app/components/_breadcrumbs.scss (100%) rename {webui => frontend}/scss/app/components/_card.scss (100%) rename {webui => frontend}/scss/app/components/_chip.scss (100%) rename {webui => frontend}/scss/app/components/_collapse_panel.scss (100%) rename {webui => frontend}/scss/app/components/_custom_dialog.scss (97%) rename {webui => frontend}/scss/app/components/_dialog.scss (100%) rename {webui => frontend}/scss/app/components/_drop_down_icon_button.scss (100%) rename {webui => frontend}/scss/app/components/_filter.scss (100%) rename {webui => frontend}/scss/app/components/_hide_content.scss (100%) rename {webui => frontend}/scss/app/components/_icon_button.scss (100%) rename {webui => frontend}/scss/app/components/_input.scss (100%) rename {webui => frontend}/scss/app/components/_list_view.scss (100%) rename {webui => frontend}/scss/app/components/_loading_indicator.scss (100%) rename {webui => frontend}/scss/app/components/_login.scss (100%) rename {webui => frontend}/scss/app/components/_mapper_script.scss (97%) rename {webui => frontend}/scss/app/components/_menu_item.scss (100%) rename {webui => frontend}/scss/app/components/_no_content.scss (100%) rename {webui => frontend}/scss/app/components/_panel.scss (100%) rename {webui => frontend}/scss/app/components/_popup_menu.scss (100%) rename {webui => frontend}/scss/app/components/_preferences.scss (100%) rename {webui => frontend}/scss/app/components/_radio_button_group.scss (100%) rename {webui => frontend}/scss/app/components/_reveal_content.scss (100%) rename {webui => frontend}/scss/app/components/_search.scss (100%) rename {webui => frontend}/scss/app/components/_sidebar.scss (100%) rename {webui => frontend}/scss/app/components/_table.scss (100%) rename {webui => frontend}/scss/app/components/_tag_list.scss (100%) rename {webui => frontend}/scss/app/components/_text_button.scss (100%) rename {webui => frontend}/scss/app/components/_toastr.scss (100%) rename {webui => frontend}/scss/app/components/_toggle_switch.scss (100%) rename {webui => frontend}/scss/app/components/dashboard/_action_card.scss (100%) rename {webui => frontend}/scss/app/components/dashboard/_dashboard_view.scss (100%) rename {webui => frontend}/scss/app/components/dashboard/_stats_view.scss (100%) rename {webui => frontend}/scss/app/components/dashboard/_status_card.scss (100%) rename {webui => frontend}/scss/app/components/playlist/_playlist_editor_view.scss (100%) rename {webui => frontend}/scss/app/components/playlist/_playlist_explorer.scss (100%) rename {webui => frontend}/scss/app/components/playlist/_playlist_explorer_view.scss (100%) create mode 100644 frontend/scss/app/components/playlist/_playlist_mappings.scss rename {webui => frontend}/scss/app/components/playlist/_playlist_processing.scss (100%) rename {webui => frontend}/scss/app/components/playlist/_playlist_update_view.scss (97%) rename {webui => frontend}/scss/app/components/playlist/_source_selector.scss (100%) rename {webui => frontend}/scss/app/components/playlist/assistant/_playlist_assistant.scss (100%) rename {webui => frontend}/scss/app/components/playlist/input/_input_table.scss (100%) rename {webui => frontend}/scss/app/components/playlist/input/_input_type.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_hdhomerun_output.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_m3u_output.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_strm_output.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_target_common.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_target_options.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_target_output.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_target_rename.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_target_sort.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_target_table.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_target_watch.scss (100%) rename {webui => frontend}/scss/app/components/playlist/target/_xtream_output.scss (100%) rename {webui => frontend}/scss/app/components/userlist/_max_connections.scss (100%) rename {webui => frontend}/scss/app/components/userlist/_proxy_type.scss (100%) rename {webui => frontend}/scss/app/components/userlist/_user_state.scss (100%) rename {webui => frontend}/scss/app/components/userlist/_userlist_view.scss (100%) rename {webui => frontend}/scss/main.scss (100%) delete mode 100644 frontend/src/api/api-service.ts delete mode 100644 frontend/src/api/auth-api-service.ts delete mode 100644 frontend/src/api/config-api-service.ts delete mode 100644 frontend/src/api/file-api-service.ts delete mode 100644 frontend/src/api/playlist-api-service.ts delete mode 100644 frontend/src/api/server-status-api-service.ts delete mode 100644 frontend/src/api/user-config-api-service.ts delete mode 100755 frontend/src/app-user/app-user.scss delete mode 100755 frontend/src/app-user/app-user.tsx delete mode 100755 frontend/src/app/app.scss delete mode 100755 frontend/src/app/app.tsx rename {webui => frontend}/src/app/components/authentication.rs (100%) rename {webui => frontend}/src/app/components/breadcrumbs.rs (100%) rename {webui => frontend}/src/app/components/card.rs (100%) rename {webui => frontend}/src/app/components/chip.rs (100%) rename {webui => frontend}/src/app/components/collapse_panel.rs (100%) rename {webui => frontend}/src/app/components/confirm_dialog.rs (100%) rename {webui => frontend}/src/app/components/content_dialog.rs (100%) rename {webui => frontend}/src/app/components/custom_dialog.rs (100%) rename {webui => frontend}/src/app/components/dashboard/action_card.rs (100%) rename {webui => frontend}/src/app/components/dashboard/dashboard_view.rs (100%) rename {webui => frontend}/src/app/components/dashboard/discord_action_card.rs (100%) rename {webui => frontend}/src/app/components/dashboard/documentation_action_card.rs (100%) rename {webui => frontend}/src/app/components/dashboard/github_action_card.rs (100%) rename {webui => frontend}/src/app/components/dashboard/ipinfo_action_card.rs (100%) rename {webui => frontend}/src/app/components/dashboard/mod.rs (100%) rename {webui => frontend}/src/app/components/dashboard/playlist_progress_status_card.rs (100%) rename {webui => frontend}/src/app/components/dashboard/stats_view.rs (100%) rename {webui => frontend}/src/app/components/dashboard/status_card.rs (100%) rename {webui => frontend}/src/app/components/dashboard/user_action_card.rs (100%) rename {webui => frontend}/src/app/components/dashboard/version_action_card.rs (100%) rename {webui => frontend}/src/app/components/drop_down_icon_button.rs (100%) rename {webui => frontend}/src/app/components/hide_content.rs (100%) rename {webui => frontend}/src/app/components/home.rs (100%) rename {webui => frontend}/src/app/components/icon_button.rs (100%) rename {webui => frontend}/src/app/components/input.rs (100%) rename {webui => frontend}/src/app/components/loading_indicator.rs (100%) rename {webui => frontend}/src/app/components/login.rs (100%) rename {webui => frontend}/src/app/components/menu_item.rs (100%) rename {webui => frontend}/src/app/components/mod.rs (100%) rename {webui => frontend}/src/app/components/no_content.rs (100%) rename {webui => frontend}/src/app/components/panel.rs (100%) rename {webui => frontend}/src/app/components/playlist/assistant/mod.rs (100%) rename {webui => frontend}/src/app/components/playlist/assistant/playlist.rs (100%) rename {webui => frontend}/src/app/components/playlist/assistant/playlist_assistant_context.rs (100%) rename {webui => frontend}/src/app/components/playlist/assistant/step_name.rs (100%) rename {webui => frontend}/src/app/components/playlist/assistant/step_schedule.rs (100%) rename {webui => frontend}/src/app/components/playlist/assistant/step_type.rs (100%) rename {webui => frontend}/src/app/components/playlist/create.rs (100%) rename {webui => frontend}/src/app/components/playlist/filter_view.rs (100%) rename {webui => frontend}/src/app/components/playlist/input/input_headers.rs (100%) rename {webui => frontend}/src/app/components/playlist/input/input_options.rs (100%) rename {webui => frontend}/src/app/components/playlist/input/input_type_view.rs (100%) rename {webui => frontend}/src/app/components/playlist/input/mod.rs (100%) rename {webui => frontend}/src/app/components/playlist/input_table.rs (100%) rename {webui => frontend}/src/app/components/playlist/list.rs (100%) rename {webui => frontend}/src/app/components/playlist/mapper_script_view.rs (86%) rename {webui => frontend}/src/app/components/playlist/mappings.rs (92%) rename {webui => frontend}/src/app/components/playlist/mod.rs (100%) rename {webui => frontend}/src/app/components/playlist/playlist_editor_page.rs (100%) rename {webui => frontend}/src/app/components/playlist/playlist_editor_view.rs (100%) rename {webui => frontend}/src/app/components/playlist/playlist_explorer.rs (100%) rename {webui => frontend}/src/app/components/playlist/playlist_explorer_page.rs (100%) rename {webui => frontend}/src/app/components/playlist/playlist_explorer_view.rs (100%) rename {webui => frontend}/src/app/components/playlist/playlist_update_view.rs (100%) rename {webui => frontend}/src/app/components/playlist/processing.rs (100%) rename {webui => frontend}/src/app/components/playlist/source_selector.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/hdhomerun_output.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/m3u_output.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/mod.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/strm_output.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/target_options.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/target_output.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/target_rename.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/target_sort.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/target_watch.rs (100%) rename {webui => frontend}/src/app/components/playlist/target/xtream_output.rs (100%) rename {webui => frontend}/src/app/components/playlist/target_table.rs (100%) rename {webui => frontend}/src/app/components/popup_menu.rs (100%) rename {webui => frontend}/src/app/components/radio_button_group.rs (100%) rename {webui => frontend}/src/app/components/reveal_content.rs (100%) rename {webui => frontend}/src/app/components/search.rs (100%) rename {webui => frontend}/src/app/components/sidebar.rs (100%) rename {webui => frontend}/src/app/components/svg_icon.rs (100%) rename {webui => frontend}/src/app/components/table.rs (100%) rename {webui => frontend}/src/app/components/tag_list.rs (100%) rename {webui => frontend}/src/app/components/text_button.rs (100%) rename {webui => frontend}/src/app/components/toastr.rs (100%) rename {webui => frontend}/src/app/components/toggle_switch.rs (100%) rename {webui => frontend}/src/app/components/userlist/edit.rs (100%) rename {webui => frontend}/src/app/components/userlist/list.rs (100%) rename {webui => frontend}/src/app/components/userlist/max_connections.rs (100%) rename {webui => frontend}/src/app/components/userlist/mod.rs (100%) rename {webui => frontend}/src/app/components/userlist/page.rs (100%) rename {webui => frontend}/src/app/components/userlist/proxy_type_view.rs (100%) rename {webui => frontend}/src/app/components/userlist/user_status.rs (100%) rename {webui => frontend}/src/app/components/userlist/user_table.rs (100%) rename {webui => frontend}/src/app/components/userlist/userlist_view.rs (100%) rename {webui => frontend}/src/app/context.rs (100%) rename {webui => frontend}/src/app/mod.rs (100%) delete mode 100644 frontend/src/component/api-proxy-view/api-proxy-view.scss delete mode 100644 frontend/src/component/api-proxy-view/api-proxy-view.tsx delete mode 100644 frontend/src/component/authentication/authentication.tsx delete mode 100644 frontend/src/component/checkbox/checkbox.scss delete mode 100644 frontend/src/component/checkbox/checkbox.tsx delete mode 100644 frontend/src/component/clipboard-viewer/clipboard-viewer.scss delete mode 100644 frontend/src/component/clipboard-viewer/clipboard-viewer.tsx delete mode 100644 frontend/src/component/date-picker/date-picker.scss delete mode 100644 frontend/src/component/date-picker/date-picker.tsx delete mode 100644 frontend/src/component/file-download/file-download.scss delete mode 100644 frontend/src/component/file-download/file-download.tsx delete mode 100644 frontend/src/component/form-view/form-view.scss delete mode 100644 frontend/src/component/form-view/from-view.tsx delete mode 100644 frontend/src/component/input-field/input-field.scss delete mode 100644 frontend/src/component/input-field/input-field.tsx delete mode 100644 frontend/src/component/loading-indicator/loading-indicator.scss delete mode 100644 frontend/src/component/loading-indicator/loading-indicator.tsx delete mode 100644 frontend/src/component/login/login.scss delete mode 100644 frontend/src/component/login/login.tsx delete mode 100644 frontend/src/component/main-config-view/main-config-view.scss delete mode 100644 frontend/src/component/main-config-view/main-config-view.tsx delete mode 100644 frontend/src/component/map-editor/map-editor.scss delete mode 100644 frontend/src/component/map-editor/map-editor.tsx delete mode 100644 frontend/src/component/panel/panel.scss delete mode 100644 frontend/src/component/panel/panel.tsx delete mode 100644 frontend/src/component/playlist-browser/playlist-browser.scss delete mode 100644 frontend/src/component/playlist-browser/playlist-browser.tsx delete mode 100755 frontend/src/component/playlist-filter/playlist-filter.scss delete mode 100755 frontend/src/component/playlist-filter/playlist-filter.tsx delete mode 100644 frontend/src/component/playlist-gallery/playlist-gallery.scss delete mode 100644 frontend/src/component/playlist-gallery/playlist-gallery.tsx delete mode 100644 frontend/src/component/playlist-tree/playlist-tree.scss delete mode 100644 frontend/src/component/playlist-tree/playlist-tree.tsx delete mode 100644 frontend/src/component/playlist-video/channel-view.scss delete mode 100644 frontend/src/component/playlist-video/channel-view.tsx delete mode 100644 frontend/src/component/playlist-video/playlist-video.tsx delete mode 100755 frontend/src/component/playlist-viewer/playlist-viewer.scss delete mode 100755 frontend/src/component/playlist-viewer/playlist-viewer.tsx delete mode 100644 frontend/src/component/popup-menu/popup-menu.scss delete mode 100644 frontend/src/component/popup-menu/popup-menu.tsx delete mode 100644 frontend/src/component/preferences/preferences.scss delete mode 100644 frontend/src/component/preferences/preferences.tsx delete mode 100644 frontend/src/component/progress/progress.scss delete mode 100644 frontend/src/component/progress/progress.tsx delete mode 100644 frontend/src/component/proxy-select/proxy-select.scss delete mode 100644 frontend/src/component/proxy-select/proxy-select.tsx delete mode 100644 frontend/src/component/schedule-editor/schedule-editor.scss delete mode 100644 frontend/src/component/schedule-editor/schedule-editor.tsx delete mode 100644 frontend/src/component/setup-wizard/_setup-common.scss delete mode 100644 frontend/src/component/setup-wizard/progress-bar/progress-bar.scss delete mode 100644 frontend/src/component/setup-wizard/progress-bar/progress-bar.tsx delete mode 100644 frontend/src/component/setup-wizard/setup-wizard.scss delete mode 100644 frontend/src/component/setup-wizard/setup-wizard.tsx delete mode 100644 frontend/src/component/setup-wizard/step-summary/step-summary.scss delete mode 100644 frontend/src/component/setup-wizard/step-summary/step-summary.tsx delete mode 100644 frontend/src/component/setup-wizard/step-welcome/step-welcome.scss delete mode 100644 frontend/src/component/setup-wizard/step-welcome/step-welcome.tsx delete mode 100644 frontend/src/component/sidebar/sidebar.scss delete mode 100644 frontend/src/component/sidebar/sidebar.tsx delete mode 100644 frontend/src/component/source-selector-editor/source-selector-editor.scss delete mode 100644 frontend/src/component/source-selector-editor/source-selector-editor.tsx delete mode 100755 frontend/src/component/source-selector/source-selector.scss delete mode 100755 frontend/src/component/source-selector/source-selector.tsx delete mode 100644 frontend/src/component/split-panel/split-panel.scss delete mode 100644 frontend/src/component/split-panel/split-panel.tsx delete mode 100644 frontend/src/component/status-view/status-view.scss delete mode 100644 frontend/src/component/status-view/status-view.tsx delete mode 100644 frontend/src/component/svg-icon/svg-icon.scss delete mode 100644 frontend/src/component/svg-icon/svg-icon.tsx delete mode 100644 frontend/src/component/tab-set/tab-set.scss delete mode 100644 frontend/src/component/tab-set/tab-set.tsx delete mode 100644 frontend/src/component/tag-input/tag-input.scss delete mode 100644 frontend/src/component/tag-input/tag-input.tsx delete mode 100644 frontend/src/component/tag-select/tag-select.scss delete mode 100644 frontend/src/component/tag-select/tags-select.tsx delete mode 100644 frontend/src/component/target-update-view/target-update-view.scss delete mode 100644 frontend/src/component/target-update-view/target-update-view.tsx delete mode 100644 frontend/src/component/tooltip/tooltip.scss delete mode 100644 frontend/src/component/tooltip/tooltip.tsx delete mode 100644 frontend/src/component/user-editor/user-editor.scss delete mode 100644 frontend/src/component/user-editor/user-editor.tsx delete mode 100755 frontend/src/component/user-playlist/user-playlist.scss delete mode 100755 frontend/src/component/user-playlist/user-playlist.tsx delete mode 100755 frontend/src/component/user-target-playlist/user-target-playlist.scss delete mode 100755 frontend/src/component/user-target-playlist/user-target-playlist.tsx delete mode 100644 frontend/src/component/user-view/user-view.scss delete mode 100644 frontend/src/component/user-view/user-view.tsx delete mode 100644 frontend/src/component/video-player/video-player.scss delete mode 100644 frontend/src/component/video-player/video-player.tsx delete mode 100644 frontend/src/config.ts rename {webui => frontend}/src/error.rs (100%) delete mode 100644 frontend/src/hook/use-const.ts delete mode 100644 frontend/src/hook/use-translator.ts rename {webui => frontend}/src/hooks/mod.rs (100%) rename {webui => frontend}/src/hooks/use_icon_context.rs (100%) rename {webui => frontend}/src/hooks/use_server_status.rs (100%) rename {webui => frontend}/src/hooks/use_service_context.rs (100%) delete mode 100644 frontend/src/icons/icons.tsx delete mode 100755 frontend/src/index.scss delete mode 100755 frontend/src/index.tsx rename {webui => frontend}/src/lib.rs (100%) rename {webui => frontend}/src/main.rs (100%) rename {webui => frontend}/src/model/busy_status.rs (100%) rename {webui => frontend}/src/model/dialog.rs (100%) rename {webui => frontend}/src/model/event_message.rs (100%) rename {webui => frontend}/src/model/explorer_source_type.rs (100%) delete mode 100644 frontend/src/model/file-download.ts delete mode 100644 frontend/src/model/message-kind.ts rename {webui => frontend}/src/model/mod.rs (100%) delete mode 100644 frontend/src/model/playlist-request.ts delete mode 100755 frontend/src/model/playlist.ts delete mode 100644 frontend/src/model/server-config.ts delete mode 100644 frontend/src/model/server-status.tsx delete mode 100755 frontend/src/model/tree-selection.ts delete mode 100644 frontend/src/model/ui-config.ts rename {webui => frontend}/src/model/view_type.rs (100%) rename {webui => frontend}/src/model/web_config.rs (100%) rename {webui => frontend}/src/provider/dialog_provider.rs (100%) rename {webui => frontend}/src/provider/icon_context_provider.rs (100%) rename {webui => frontend}/src/provider/mod.rs (100%) delete mode 100644 frontend/src/provider/service-provider.tsx rename {webui => frontend}/src/provider/service_context_provider.rs (100%) delete mode 100644 frontend/src/react-app-env.d.ts delete mode 100644 frontend/src/scss/_common.scss delete mode 100644 frontend/src/scss/_default.scss delete mode 100644 frontend/src/scss/_panel.scss delete mode 100644 frontend/src/scss/_preferences.scss delete mode 100644 frontend/src/scss/_theme.scss delete mode 100644 frontend/src/service/auth-service.ts delete mode 100644 frontend/src/service/config-service.ts delete mode 100755 frontend/src/service/file-service.ts delete mode 100644 frontend/src/service/playlist-service.ts delete mode 100644 frontend/src/service/server-status-service.ts delete mode 100644 frontend/src/service/service-context.ts delete mode 100644 frontend/src/service/user-config-service.ts rename {webui => frontend}/src/services/auth_service.rs (100%) rename {webui => frontend}/src/services/config_service.rs (100%) rename {webui => frontend}/src/services/dialog_service.rs (100%) rename {webui => frontend}/src/services/event_service.rs (100%) rename {webui => frontend}/src/services/mod.rs (100%) rename {webui => frontend}/src/services/playlist_service.rs (100%) rename {webui => frontend}/src/services/requests.rs (100%) rename {webui => frontend}/src/services/status_service.rs (100%) rename {webui => frontend}/src/services/toastr_service.rs (100%) rename {webui => frontend}/src/services/websocket_service.rs (100%) delete mode 100644 frontend/src/utils/click-away-listener.tsx delete mode 100644 frontend/src/utils/clipboard.ts delete mode 100644 frontend/src/utils/config-utils.ts delete mode 100644 frontend/src/utils/date-utils.ts delete mode 100644 frontend/src/utils/fetcher.ts delete mode 100644 frontend/src/utils/i18n.ts delete mode 100644 frontend/src/utils/icon-utils.tsx rename {webui => frontend}/src/utils/mod.rs (100%) rename {webui => frontend}/src/utils/storage.rs (100%) delete mode 100644 frontend/src/utils/text-generator.ts delete mode 100644 frontend/src/utils/uuid.ts delete mode 100644 frontend/tsconfig.json delete mode 100644 webui/public/assets/favicon.ico delete mode 100644 webui/public/assets/fonts.css delete mode 100644 webui/public/assets/fonts/rubik-arabic-400-italic.woff delete mode 100644 webui/public/assets/fonts/rubik-arabic-400-italic.woff2 delete mode 100644 webui/public/assets/fonts/rubik-arabic-400-normal.woff delete mode 100644 webui/public/assets/fonts/rubik-arabic-400-normal.woff2 delete mode 100644 webui/public/assets/fonts/rubik-cyrillic-400-italic.woff delete mode 100644 webui/public/assets/fonts/rubik-cyrillic-400-italic.woff2 delete mode 100644 webui/public/assets/fonts/rubik-cyrillic-400-normal.woff delete mode 100644 webui/public/assets/fonts/rubik-cyrillic-400-normal.woff2 delete mode 100644 webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff delete mode 100644 webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff2 delete mode 100644 webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff delete mode 100644 webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff2 delete mode 100644 webui/public/assets/fonts/rubik-hebrew-400-italic.woff delete mode 100644 webui/public/assets/fonts/rubik-hebrew-400-italic.woff2 delete mode 100644 webui/public/assets/fonts/rubik-hebrew-400-normal.woff delete mode 100644 webui/public/assets/fonts/rubik-hebrew-400-normal.woff2 delete mode 100644 webui/public/assets/fonts/rubik-latin-400-italic.woff delete mode 100644 webui/public/assets/fonts/rubik-latin-400-italic.woff2 delete mode 100644 webui/public/assets/fonts/rubik-latin-400-normal.woff delete mode 100644 webui/public/assets/fonts/rubik-latin-400-normal.woff2 delete mode 100644 webui/public/assets/fonts/rubik-latin-ext-400-italic.woff delete mode 100644 webui/public/assets/fonts/rubik-latin-ext-400-italic.woff2 delete mode 100644 webui/public/assets/fonts/rubik-latin-ext-400-normal.woff delete mode 100644 webui/public/assets/fonts/rubik-latin-ext-400-normal.woff2 delete mode 100644 webui/public/assets/missing-logo.svg delete mode 100644 webui/public/assets/placeholder.png delete mode 100644 webui/public/assets/schedule.svg delete mode 100644 webui/public/assets/tuliprox-logo.svg delete mode 100644 webui/public/config.json delete mode 100644 webui/scss/app/components/playlist/_playlist_mappings.scss diff --git a/.dockerignore b/.dockerignore index e58ec7117..7fd7123ad 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,4 +11,4 @@ docker/ frontend/node_modules/ frontend/build/ -webui/dist +frontend/dist diff --git a/Cargo.toml b/Cargo.toml index b6474de40..44e0abdd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["backend", "webui", "shared"] +members = ["backend", "frontend", "shared"] resolver = "2" [profile.release] diff --git a/bin/build_aarch64.sh b/bin/build_aarch64.sh index 8029b0565..7fc58de88 100755 --- a/bin/build_aarch64.sh +++ b/bin/build_aarch64.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash echo "building binary for aarch64" env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target aarch64-unknown-linux-musl -cd ./webui || exit 1 +cd ./frontend || exit 1 env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release diff --git a/bin/build_armv7.sh b/bin/build_armv7.sh index 9d16e3bf8..76d6a18d4 100755 --- a/bin/build_armv7.sh +++ b/bin/build_armv7.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash echo "building binary for armv7" env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target armv7-unknown-linux-musleabihf -cd ./webui || exit 1 +cd ./frontend || exit 1 env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release \ No newline at end of file diff --git a/bin/build_darwin.sh b/bin/build_darwin.sh index 0db303bfe..0fc62e75b 100755 --- a/bin/build_darwin.sh +++ b/bin/build_darwin.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target x86_64-apple-darwin -cd ./webui || exit 1 +cd ./frontend || exit 1 env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release \ No newline at end of file diff --git a/bin/build_fe.sh b/bin/build_fe.sh index 37f519626..5b71d4291 100755 --- a/bin/build_fe.sh +++ b/bin/build_fe.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -cd ./webui && rm -rf dist && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release +cd ./frontend && rm -rf dist && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release diff --git a/bin/build_github_docker.sh b/bin/build_github_docker.sh index e9a93fec1..0e27a11eb 100755 --- a/bin/build_github_docker.sh +++ b/bin/build_github_docker.sh @@ -7,7 +7,7 @@ BIN_DIR="${WORKING_DIR}/bin" RESOURCES_DIR="${WORKING_DIR}/resources" DOCKER_DIR="${WORKING_DIR}/docker" BACKEND_DIR="${WORKING_DIR}/backend" -FRONTEND_DIR="${WORKING_DIR}/webui" +FRONTEND_DIR="${WORKING_DIR}/frontend" FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist" declare -A ARCHITECTURES=( [LINUX]=x86_64-unknown-linux-musl diff --git a/bin/build_github_docker_aarch64.sh b/bin/build_github_docker_aarch64.sh index d4c418820..96ee731ae 100755 --- a/bin/build_github_docker_aarch64.sh +++ b/bin/build_github_docker_aarch64.sh @@ -28,7 +28,7 @@ BIN_DIR="${WORKING_DIR}/bin" RESOURCES_DIR="${WORKING_DIR}/resources" DOCKER_DIR="${WORKING_DIR}/docker" BACKEND_DIR="${WORKING_DIR}/backend" -FRONTEND_DIR="${WORKING_DIR}/webui" +FRONTEND_DIR="${WORKING_DIR}/frontend" FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist" TARGET=aarch64-unknown-linux-musl VERSION=$(grep -Po '^version\s*=\s*"\K[0-9\.]+' "${BACKEND_DIR}/Cargo.toml" || true) diff --git a/bin/build_github_docker_beta.sh b/bin/build_github_docker_beta.sh index d44622a61..cae303531 100755 --- a/bin/build_github_docker_beta.sh +++ b/bin/build_github_docker_beta.sh @@ -7,7 +7,7 @@ BIN_DIR="${WORKING_DIR}/bin" RESOURCES_DIR="${WORKING_DIR}/resources" DOCKER_DIR="${WORKING_DIR}/docker" BACKEND_DIR="${WORKING_DIR}/backend" -FRONTEND_DIR="${WORKING_DIR}/webui" +FRONTEND_DIR="${WORKING_DIR}/frontend" FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist" TARGET=x86_64-unknown-linux-musl VERSION=$(grep -Po '^version\s*=\s*"\K[0-9\.]+' "${BACKEND_DIR}/Cargo.toml" || true) diff --git a/bin/build_lin.sh b/bin/build_lin.sh index d6184d81e..c32b029c7 100755 --- a/bin/build_lin.sh +++ b/bin/build_lin.sh @@ -5,4 +5,4 @@ if [ "$(uname)" != "Linux" ]; then else cargo build -p tuliprox --release fi -cd "./webui" && trunk build --release \ No newline at end of file +cd "./frontend" && trunk build --release \ No newline at end of file diff --git a/bin/build_lin_static.sh b/bin/build_lin_static.sh index 2bcf7c2a6..fca88dcca 100755 --- a/bin/build_lin_static.sh +++ b/bin/build_lin_static.sh @@ -2,4 +2,4 @@ DEFAULT_TARGET="x86_64-unknown-linux-musl" TARGET="${1:-$DEFAULT_TARGET}" env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target "$TARGET" -cd "./webui" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release \ No newline at end of file +cd "./frontend" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release \ No newline at end of file diff --git a/bin/build_win.sh b/bin/build_win.sh index 5e6e3fbc2..bff62549e 100755 --- a/bin/build_win.sh +++ b/bin/build_win.sh @@ -3,4 +3,4 @@ env RUSTFLAGS="--remap-path-prefix $HOME=~" cargo build -p tuliprox --release -- # cross build, is only working with cargo clean # env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target x86_64-pc-windows-gnu -cd "./webui" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release +cd "./frontend" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release diff --git a/bin/release.sh b/bin/release.sh index a0e8919ec..a1df08166 100755 --- a/bin/release.sh +++ b/bin/release.sh @@ -5,7 +5,7 @@ set -o pipefail WORKING_DIR=$(pwd) RESOURCES_DIR="$WORKING_DIR/resources" RELEASE_DIR="$WORKING_DIR/release" -FRONTEND_DIR="${WORKING_DIR}/webui" +FRONTEND_DIR="${WORKING_DIR}/frontend" FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist" if ! command -v cargo-set-version &> /dev/null diff --git a/docker/Dockerfile b/docker/Dockerfile index 51e058c74..4254aba6a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -58,7 +58,7 @@ RUN cargo install --locked trunk wasm-bindgen-cli # Now copy the actual source code and build the project COPY . . -WORKDIR /src/webui +WORKDIR /src/frontend # Run Trunk build RUN trunk build --release @@ -135,7 +135,7 @@ RUN rustup update && rustup target add $RUST_TARGET # Create the final application layout to mimic production WORKDIR /app -COPY --from=trunk-build /src/webui/dist ./web +COPY --from=trunk-build /src/frontend/dist ./web COPY --from=resource-build /src/resources ./resources # Create a separate directory for the source code @@ -170,7 +170,7 @@ COPY --from=rust-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ # App WORKDIR /app COPY --from=rust-build /src/target/${RUST_TARGET}/release/tuliprox ./tuliprox -COPY --from=trunk-build /src/webui/dist ./web +COPY --from=trunk-build /src/frontend/dist ./web COPY --from=resource-build /src/resources ./resources ENTRYPOINT ["/app/tuliprox"] @@ -195,7 +195,7 @@ RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime WORKDIR /app COPY --from=rust-build /src/target/${RUST_TARGET}/release/tuliprox ./tuliprox -COPY --from=trunk-build /src/webui/dist ./web +COPY --from=trunk-build /src/frontend/dist ./web COPY --from=resource-build /src/resources ./resources ENTRYPOINT ["/sbin/tini", "--", "/app/tuliprox"] diff --git a/webui/Cargo.toml b/frontend/Cargo.toml similarity index 100% rename from webui/Cargo.toml rename to frontend/Cargo.toml diff --git a/webui/Trunk.toml b/frontend/Trunk.toml similarity index 100% rename from webui/Trunk.toml rename to frontend/Trunk.toml diff --git a/frontend/eslintrc.json b/frontend/eslintrc.json deleted file mode 100644 index 3bd6d3171..000000000 --- a/frontend/eslintrc.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "env": { - "jest/globals": true - }, - "parser": "@typescript-eslint/parser", - "extends": [ - "react-app", - "react-app/jest", - "eslint-config-prettier", - "plugin:react/recommended" - ], - "settings": { - "react": { - "createClass": "createReactClass", - "pragma": "React", - "fragment": "Fragment", - "version": "detect", - "flowVersion": "detect" - } - }, - "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module", - "ecmaFeatures": { - "jsx": true - } - }, - "plugins": [ - "import", - "node", - "react" - ], - "rules": { - "consistent-this": [ - "error", - "self" - ], - "@typescript-eslint/no-this-alias": [ - "error", - { - "allowDestructuring": false, - "allowedNames": [ - "self" - ] - } - ], - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/ban-ts-ignore": "off", - "react/display-name": "off", - "react/prop-types": "off" - }, - "ignorePatterns": [ "*.test.tsc", "*.scss", "*.css" ] -} diff --git a/webui/index.html b/frontend/index.html similarity index 100% rename from webui/index.html rename to frontend/index.html diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100755 index d128c6de5..000000000 --- a/frontend/package.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "name": "tuliprox", - "description": "tuliprox", - "author": "euzu", - "version": "3.1.4", - "private": true, - "homepage": "", - "license": "MIT", - "scripts": { - "react-start": "react-scripts start", - "react-build": "react-scripts build", - "react-test": "react-scripts test --env=jsdom", - "react-eject": "react-scripts eject", - "build": "cross-env GENERATE_SOURCEMAP=false REACT_APP_STAGE=production PUBLIC_URL= yarn react-build", - "start": "yarn react-start", - "lint": "eslint --config eslintrc.json src/** --no-error-on-unmatched-pattern --quiet", - "upgr": "yarn upgrade-interactive --latest", - "analyze": "source-map-explorer 'build/static/js/*.js'" - }, - "dependencies": { - "file-saver": "2.0.5", - "i18next": "25.0.1", - "jwt-decode": "4.0.0", - "mpegts.js": "1.8.0", - "notistack": "3.0.2", - "react": "19.1.0", - "react-datepicker": "8.3.0", - "react-dom": "19.1.0", - "react-i18next": "15.4.1", - "react-tag-input-component": "2.0.2", - "rxjs": "7.8.2", - "video.js": "8.22.0", - "videojs-mpegtsjs": "1.0.0" - }, - "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "7.21.11", - "@babel/runtime": "7.27.0", - "@types/file-saver": "2.0.7", - "@types/react": "19.1.2", - "@types/react-dom": "19.1.2", - "@types/video.js": "7.3.58", - "@typescript-eslint/eslint-plugin": "8.30.1", - "@typescript-eslint/parser": "8.30.1", - "babel-eslint": "10.1.0", - "concurrently": "9.1.2", - "cross-env": "7.0.3", - "eslint": "9.25.0", - "eslint-config-prettier": "10.0.1", - "eslint-config-react-app": "7.0.1", - "eslint-plugin-flowtype": "8.0.3", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-jest": "28.11.0", - "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-react": "7.37.4", - "eslint-plugin-react-hooks": "5.2.0", - "prettier": "3.5.3", - "react-scripts": "5.0.1", - "sass": "1.86.3", - "source-map-explorer": "2.5.3", - "typescript": "5.8.3", - "wait-on": "8.0.3" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "proxy": "http://localhost:8901" -} diff --git a/frontend/public/assets/fonts.css b/frontend/public/assets/fonts.css index fd07a74a0..01d5d09a4 100644 --- a/frontend/public/assets/fonts.css +++ b/frontend/public/assets/fonts.css @@ -56,4 +56,4 @@ font-weight: 400; src: url('fonts/rubik-latin-400-normal.woff2') format('woff2'), url('fonts/rubik-latin-400-normal.woff') format('woff'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; -} \ No newline at end of file +} diff --git a/frontend/public/assets/i18n/de_common.json b/frontend/public/assets/i18n/de_common.json deleted file mode 100644 index 134908033..000000000 --- a/frontend/public/assets/i18n/de_common.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "LABEL": { - "LIVE": "Live", - "VOD": "VOD", - "SERIES": "Serien", - "SAVE": "Speichern", - "SUBMIT": "Übernehmen", - "OK": "Ok", - "CANCEL": "Abbrechen", - "SHOW_SELECTED": "Ausgewählte anzeigen", - "SELECT_ALL": "Alle auswählen", - "DESELECT_ALL": "Alle abwählen", - "SEARCH": "Suche", - "CLEAR": "Löschen", - "REGEXP": "RegExp", - "LOGOUT": "Abmelden", - "CONFIGURATION": "Konfiguration", - "HOST": "Host", - "PORT": "Port", - "WEB_ROOT": "Web-Root", - "THREADS": "Threads", - "WORKING_DIR": "Arbeitsverzeichnis", - "BACKUP_DIR": "Backup-Verzeichnis", - "UPDATE_ON_BOOT": "Update beim Start", - "WEB_UI": "Web-UI", - "SCHEDULES": "Zeitpläne", - "SANITIZE_SENSITIVE_INFO": "Sensiblen Inhalt bereinigen", - "ACTIVE_CLIENTS": "Aktive Clients", - "RESOURCE_REWRITE_DISABLE": "Ressourcenumschreibung deaktivieren", - "RECONNECT": "Erneut verbinden", - "BUFFER_ENABLED": "Puffer aktiviert", - "BUFFER_SIZE": "Puffergröße", - "ENABLED": "Aktiviert", - "SIZE": "Größe", - "CACHE_DIR": "Cache-Verzeichnis", - "NOTIFY_ON": "Benachrichtigen bei", - "BOT_TOKEN": "Bot-Token", - "CHAT_IDS": "Chat-IDs", - "URL": "URL", - "USER": "Benutzer", - "USERS": "Benutzer", - "TOKEN": "Token", - "WEB_SEARCH_URL": "Web-Such-URL", - "EXTENSIONS": "Erweiterungen", - "HEADERS": "Header", - "DIRECTORY": "Verzeichnis", - "ORGANIZE_INTO_DIRECTORIES": "In Verzeichnisse organisieren", - "EPISODE_PATTERN": "Episoden-Muster", - "API": "API", - "MAIN": "Haupt", - "REVERSE_PROXY": "ReverseProxy", - "MESSAGING": "Nachrichten", - "VIDEO": "Video", - "EXAMPLES": "Beispiele", - "UPDATE": "Aktualisieren", - "START": "Starten", - "CACHE": "Cache", - "STREAM": "Stream", - "LOG": "Protokoll", - "CONFIG": "Konfiguration", - "TELEGRAM": "Telegram", - "REST": "Rest", - "PUSHOVER": "Pushover", - "PROXY": "Proxy", - "REFRESH": "Aktualisieren", - "STATUS": "Status", - "MAX_CON": "MaxCon", - "EXP_DATE": "Ablaufdatum", - "USERNAME": "Benutzername", - "PASSWORD": "Passwort", - "SERVER": "Server", - "TIMEZONE": "Zeitzone", - "PATH": "Pfad", - "MESSAGE": "Nachricht", - "PROTOCOL": "Protokoll", - "NAME": "Name" - }, - "TITLE": { - "USER_BOUQUET_EDITOR": "Benutzer-Gruppen-Editor" - }, - "MESSAGES": { - "INVALID_REGEXP": "Ungültiger regulärer Ausdruck", - "INVALID_FILETYPE": "Ungültiger Dateityp", - "DOWNLOAD": { - "SUCCESS": "Erfolgreich heruntergeladen", - "FAIL": "Fehler beim Herunterladen!", - "PLAYLIST": { - "SUCCESS": "Playlist erfolgreich heruntergeladen", - "FAIL": "Fehler beim Herunterladen der Playlist!" - }, - "SERVER_CONFIG": { - "FAIL": "Fehler beim Herunterladen der Serverkonfiguration!" - } - }, - "SAVE": { - "MAIN_CONFIG": { - "SUCCESS": "Hauptkonfiguration gespeichert!", - "FAIL": "Fehler beim Speichern der Hauptkonfiguration!" - }, - "BOUQUET": { - "SUCCESS": "Bouquet erfolgreich gespeichert", - "FAIL": "Fehler beim Speichern des Bouquets!" - }, - "USER": { - "SUCCESS": "Benutzer erfolgreich gespeichert", - "FAIL": "Fehler beim Speichern des Benutzers!" - }, - "PLAYLIST_UPDATE": { - "SUCCESS": "Playlist Aktualisierung erfolgreich gestartet!", - "FAIL": "Playlist Aktualisierung fehlgeschlagen!" - }, - "SOURCE_SELECTOR": { - "MISSING_SELECTION": "Kein Eintrag ausgewählt" - } - } - }, - "INFO": { - "RESTART_TO_APPLY_CHANGES": "Sie müssen neu starten, um Änderungen anzuwenden.", - "SCHEDULE_EXAMPLE": "Beispiel für Zeitplan", - "THREADS": "Threads: Lassen Sie es auf 0, wenn keine Probleme bestehen. Erhöhen Sie es, wenn Sie mehrere Anbieter haben. Der parallele Zugriff auf Ihren Anbieter kann zu einer Sperrung führen.", - "WEB_ROOT": "Web-Root: Enthält die Web-UI-Dateien. Normalerweise befinden sie sich im gleichen Verzeichnis unter ./web" - } -} diff --git a/webui/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json similarity index 100% rename from webui/public/assets/i18n/en.json rename to frontend/public/assets/i18n/en.json diff --git a/frontend/public/assets/i18n/en_common.json b/frontend/public/assets/i18n/en_common.json deleted file mode 100644 index 09dde894e..000000000 --- a/frontend/public/assets/i18n/en_common.json +++ /dev/null @@ -1,206 +0,0 @@ -{ - "LABEL": { - "LIVE": "Live", - "VOD": "VOD", - "SERIES": "Series", - "SAVE": "Save", - "SUBMIT": "Submit", - "OK": "Ok", - "CANCEL": "Cancel", - "CLOSE": "Close", - "SHOW_SELECTED": "Show selected", - "SELECT_ALL": "Select All", - "DESELECT_ALL": "Deselect all", - "SEARCH": "Search", - "CLEAR": "Clear", - "REGEXP": "Regexp", - "LOGOUT": "Logout", - "CONFIGURATION": "Configuration", - "HOST": "Host", - "PORT": "Port", - "WEB_ROOT": "Web-Root", - "THREADS": "Threads", - "WORKING_DIR": "Working dir", - "BACKUP_DIR": "Backup dir", - "UPDATE_ON_BOOT": "Update on boot", - "WEB_UI": "Web UI", - "USER_UI": "User UI", - "WEB_UI_PATH": "Path prefix", - "SCHEDULES": "Schedules", - "SANITIZE_SENSITIVE_INFO": "Sanitize sensitive info", - "ACTIVE_USER": "Active User", - "RESOURCE_REWRITE_DISABLE": "Resource Rewrite disable", - "DISABLE_REFERER_HEADER": "Referer header disable", - "RECONNECT": "Reconnect", - "THROTTLE": "Throttle", - "GRACE_PERIOD": "Grace period ms", - "GRACE_PERIOD_TIMEOUT": "Grace period timeout sec", - "BUFFER_ENABLED": "Buffer Enabled", - "BUFFER_SIZE": "Buffer Size", - "ENABLED": "Enabled", - "SIZE": "Size", - "CACHE_DIR": "Cache dir", - "NOTIFY_ON": "Notify on", - "BOT_TOKEN": "Bot Token", - "CHAT_IDS": "Chat Ids", - "URL": "Url", - "USER": "User", - "USERS": "Users", - "TOKEN": "Token", - "WEB_SEARCH_URL": "Web Search Url", - "EXTENSIONS": "Extensions", - "HEADERS": "Headers", - "DIRECTORY": "Directory", - "ORGANIZE_INTO_DIRECTORIES": "Organize Into Directorie", - "EPISODE_PATTERN": "Episode Pattern", - "API": "Api", - "MAIN": "Main", - "REVERSE_PROXY": "ReverseProxy", - "MESSAGING": "Messaging", - "VIDEO": "Video", - "EXAMPLES": "Examples", - "UPDATE": "Update", - "START": "Start", - "CACHE": "Cache", - "STREAM": "Stream", - "LOG": "Log", - "LOG_LEVEL": "Log level", - "CONFIG": "Config", - "TELEGRAM": "Telegram", - "REST": "Rest", - "PUSHOVER": "Pushover", - "PROXY": "Proxy", - "REFRESH": "Refresh", - "STATUS": "Status", - "MAX_CON": "MaxCon", - "EXP_DATE": "ExpDate", - "USERNAME": "Username", - "PASSWORD": "Password", - "SERVER": "Server", - "TIMEZONE": "Timezone", - "PATH": "Path", - "MESSAGE": "Message", - "PROTOCOL": "Protocol", - "NAME": "Name", - "API_PROXY": "Api-Proxy", - "INFINITE": "Infinite", - "ONE_YEAR": "One year", - "SELF_HOSTED": "Self Hosted", - "PROVIDER": "Provider", - "CUSTOM": "Custom", - "XTREAM_CODES": "xtream codes", - "XC": "xc", - "M3U": "m3u", - "PLAYLIST_BROWSER": "Playlist Browser", - "RATE_LIMIT": "Rate Limit", - "PERIOD_MILLIS": "Period Millis", - "BURST_SIZE": "Burst Size", - "WEB_AUTH": "Auth", - "ISSUER": "Issuer", - "SECRET": "Secret", - "USERFILE": "Userfile", - "UI_ENABLED": "UI", - "PLAYER_SERVER": "Player Server", - "VERSION": "Version", - "SERVER_TIME": "Server time", - "BUILD_TIME": "Build time", - "MEMORY": "Memory", - "ACTIVE_USERS": "Active users", - "ACTIVE_USER_CONNECTIONS": "Active user conn...", - "ACTIVE_PROVIDER_CONNECTIONS": "Active provider conn...", - "IP_CHECK": "IP Check", - "URL_IPV4": "Url IPv4", - "URL_IPV6": "Url IPv6", - "PATTERN_IPV4": "Pattern IPv4", - "PATTERN_IPV6": "Pattern IPv6", - "IPv4": "IPv4", - "IPv6": "IPv6", - "IP": "IP", - "TARGET": "Target", - "TARGETS": "Targets", - "NOTES": "Notes" - }, - "TITLE": { - "USER_BOUQUET_EDITOR": "User group editor" - }, - "MESSAGES": { - "NO_CONTENT": "No content", - "INVALID_REGEXP": "Invalid regexp", - "INVALID_FILETYPE": "Invalid filetype", - "TARGET_NOT_EXISTS": "Target does not exist", - "DOWNLOAD": { - "SUCCESS": "Successfully downloaded", - "FAIL": "Failed to download!", - "PLAYLIST": { - "SUCCESS": "Successfully downloaded playlist", - "FAIL": "Failed to download playlist!" - }, - "SERVER_CONFIG": { - "FAIL": "Failed to download server config!" - } - }, - "SAVE": { - "MAIN_CONFIG": { - "SUCCESS": "Main config saved!", - "FAIL": "Failed to save main config!" - }, - "BOUQUET": { - "SUCCESS": "Successfully saved bouquet", - "FAIL": "Failed to save bouquet!" - }, - "USER": { - "SUCCESS": "Successfully saved user", - "FAIL": "Failed to save user!", - "USERNAME_REQUIRED": "Username required", - "DUPLICATE_USERNAME": "Duplicate username!", - "DUPLICATE_TOKEN": "Duplicate token!", - "TARGET_NOT_FOUND": "Target not found!", - "MAX_CONNECTIONS_INVALID": "MaxConnections invalid" - } - }, - "PLAYLIST_UPDATE": { - "SUCCESS": "Successfully started playlist update!", - "FAIL": "Playlist update failed!" - }, - "SOURCE_SELECTOR": { - "MISSING_SELECTION": "No entry selected" - } - }, - "INFO": { - "RESTART_TO_APPLY_CHANGES": "You need to restart to apply changes.", - "SCHEDULE_EXAMPLE": "Schedule example", - "THREADS": "Threads: keep it 0 if you have no problems. Increase it if you have multiple providers. Accessing you provider parallel can cause a ban.", - "WEB_ROOT": "Web-Root: Contains the web-ui files. They are normally placed in the same directory under ./web" - }, - "HINT": { - "CONFIG": { - "API_PROXY": { - "PATH": "URL path prefix" - }, - "USER": { - "TARGET_FORCE_REDIRECT": "Force Redirect is set for target" - }, - "LOG": { - "ACTIVE_USER": "Activates active user info log" - }, - "PROXY": { - "RESOURCE_REWRITE_DISABLE": "If activated, the URL rewrite of resources in reverse proxy mode is disabled", - "DISABLE_REFERER_HEADER": "If activated, the referer header is not sent to the provider", - "THROTTLE": "Supported units are KB/s,MB/s,KiB/s,MiB/s,kbps,mbps,Mibps" - }, - "WEB_UI": { - "PATH": "Configuration of a path prefix, allowing it to be accessed behind a reverse proxy under a custom subpath. like https://yourdomain.com/tuliprox" - } - } - }, - "SETUP":{ - "LABEL": { - "NEXT": "Next", - "BACK": "Back", - "FINISH_SETUP": "Finish Setup" - }, - "MSG": { - "WELCOME": "Welcome to tuliprox setup. This wizard will guide you through the initial setup process." - } - } -} \ No newline at end of file diff --git a/frontend/public/assets/i18n/es_common.json b/frontend/public/assets/i18n/es_common.json deleted file mode 100644 index 1b336e84c..000000000 --- a/frontend/public/assets/i18n/es_common.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "LABEL": { - "LIVE": "En vivo", - "VOD": "VOD", - "SERIES": "Series", - "SAVE": "Guardar", - "SHOW_SELECTED": "Mostrar seleccionados", - "SELECT_ALL": "Seleccionar todo", - "DESELECT_ALL": "Deseleccionar todo", - "SEARCH": "Buscar", - "CLEAR": "Borrar", - "REGEXP": "Expresión regular", - "LOGOUT": "Cerrar sesión", - "CONFIGURATION": "Configuración", - "HOST": "Host", - "PORT": "Puerto", - "WEB_ROOT": "Raíz web", - "THREADS": "Hilos", - "WORKING_DIR": "Directorio de trabajo", - "BACKUP_DIR": "Directorio de respaldo", - "UPDATE_ON_BOOT": "Actualizar al arrancar", - "WEB_UI": "Interfaz web", - "SCHEDULES": "Horarios", - "SANITIZE_SENSITIVE_INFO": "Sanitizar información sensible", - "ACTIVE_CLIENTS": "Clientes activos", - "RESOURCE_REWRITE_DISABLE": "Desactivar reescritura de recursos", - "RECONNECT": "Reconectar", - "BUFFER_ENABLED": "Búfer activado", - "BUFFER_SIZE": "Tamaño del búfer", - "ENABLED": "Habilitado", - "SIZE": "Tamaño", - "CACHE_DIR": "Directorio de caché", - "NOTIFY_ON": "Notificar en", - "BOT_TOKEN": "Token del bot", - "CHAT_IDS": "IDs de chat", - "URL": "URL", - "USER": "Usuario", - "TOKEN": "Token", - "WEB_SEARCH_URL": "URL de búsqueda web", - "EXTENSIONS": "Extensiones", - "HEADERS": "Encabezados", - "DIRECTORY": "Directorio", - "ORGANIZE_INTO_DIRECTORIES": "Organizar en directorios", - "EPISODE_PATTERN": "Patrón de episodio", - "API": "API", - "MAIN": "Principal", - "REVERSE_PROXY": "Proxy inverso", - "MESSAGING": "Mensajería", - "VIDEO": "Video", - "EXAMPLES": "Ejemplos", - "UPDATE": "Actualizar", - "START": "Iniciar", - "CACHE": "Caché", - "STREAM": "Flujo", - "LOG": "Registro", - "CONFIG": "Configuración", - "TELEGRAM": "Telegram", - "REST": "Rest", - "PUSHOVER": "Pushover", - "PROXY": "Proxy", - "REFRESH": "Actualizar", - "STATUS": "Estado", - "MAX_CON": "MaxCon", - "EXP_DATE": "Fecha de expiración", - "USERNAME": "Nombre de usuario", - "PASSWORD": "Contraseña", - "SERVER": "Servidor", - "TIMEZONE": "Zona horaria", - "PATH": "Ruta", - "MESSAGE": "Mensaje", - "PROTOCOL": "Protocolo", - "NAME": "Nombre" - }, - "TITLE": { - "USER_BOUQUET_EDITOR": "Editor de bouquet de usuario" - }, - "MESSAGES": { - "INVALID_REGEXP": "Expresión regular no válida", - "INVALID_FILETYPE": "Tipo de archivo no válido", - "DOWNLOAD": { - "SUCCESS": "Descargado con éxito", - "FAIL": "¡Error al descargar!", - "PLAYLIST": { - "SUCCESS": "Lista de reproducción descargada con éxito", - "FAIL": "¡Error al descargar la lista de reproducción!" - }, - "SERVER_CONFIG": { - "FAIL": "¡Error al descargar la configuración del servidor!" - } - }, - "SAVE": { - "MAIN_CONFIG": { - "SUCCESS": "¡Configuración principal guardada!", - "FAIL": "¡Error al guardar la configuración principal!" - }, - "BOUQUET": { - "SUCCESS": "Bouquet guardado con éxito", - "FAIL": "¡Error al guardar el bouquet!" - }, - "USER": { - "SUCCESS": "Usuario guardado con éxito", - "FAIL": "¡Error al guardar el usuario!" - } - } - }, - "INFO": { - "RESTART_TO_APPLY_CHANGES": "Necesitas reiniciar para aplicar los cambios.", - "SCHEDULE_EXAMPLE": "Ejemplo de horario", - "THREADS": "Hilos: mantén en 0 si no tienes problemas. Aumenta si tienes varios proveedores. Acceder a tu proveedor en paralelo puede causar un baneo.", - "WEB_ROOT": "Raíz web: contiene los archivos de la interfaz web. Normalmente se colocan en el mismo directorio bajo ./web" - } -} diff --git a/frontend/public/assets/i18n/fr_common.json b/frontend/public/assets/i18n/fr_common.json deleted file mode 100644 index f1c5c33dc..000000000 --- a/frontend/public/assets/i18n/fr_common.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "LABEL": { - "LIVE": "En direct", - "VOD": "VOD", - "SERIES": "Séries", - "SAVE": "Enregistrer", - "SHOW_SELECTED": "Afficher la sélection", - "SELECT_ALL": "Tout sélectionner", - "DESELECT_ALL": "Tout désélectionner", - "SEARCH": "Rechercher", - "CLEAR": "Effacer", - "REGEXP": "Expression régulière", - "LOGOUT": "Se déconnecter", - "CONFIGURATION": "Configuration", - "HOST": "Hôte", - "PORT": "Port", - "WEB_ROOT": "Racine Web", - "THREADS": "Threads", - "WORKING_DIR": "Répertoire de travail", - "BACKUP_DIR": "Répertoire de sauvegarde", - "UPDATE_ON_BOOT": "Mettre à jour au démarrage", - "WEB_UI": "Interface Web", - "SCHEDULES": "Programmes", - "SANITIZE_SENSITIVE_INFO": "Assainir les informations sensibles", - "ACTIVE_CLIENTS": "Clients actifs", - "RESOURCE_REWRITE_DISABLE": "Désactiver la réécriture des ressources", - "RECONNECT": "Se reconnecter", - "BUFFER_ENABLED": "Mise en mémoire tampon activée", - "BUFFER_SIZE": "Taille du tampon", - "ENABLED": "Activé", - "SIZE": "Taille", - "CACHE_DIR": "Répertoire du cache", - "NOTIFY_ON": "Notifier lorsqu'il y a", - "BOT_TOKEN": "Jeton du bot", - "CHAT_IDS": "Identifiants de chat", - "URL": "URL", - "USER": "Utilisateur", - "TOKEN": "Jeton", - "WEB_SEARCH_URL": "URL de recherche Web", - "EXTENSIONS": "Extensions", - "HEADERS": "En-têtes", - "DIRECTORY": "Répertoire", - "ORGANIZE_INTO_DIRECTORIES": "Organiser dans des répertoires", - "EPISODE_PATTERN": "Modèle d'épisode", - "API": "API", - "MAIN": "Principal", - "REVERSE_PROXY": "Proxy inverse", - "MESSAGING": "Messagerie", - "VIDEO": "Vidéo", - "EXAMPLES": "Exemples", - "UPDATE": "Mettre à jour", - "START": "Démarrer", - "CACHE": "Cache", - "STREAM": "Diffusion", - "LOG": "Journal", - "CONFIG": "Configuration", - "TELEGRAM": "Telegram", - "REST": "Rest", - "PUSHOVER": "Pushover", - "PROXY": "Proxy", - "REFRESH": "Rafraîchir", - "STATUS": "Statut", - "MAX_CON": "MaxCon", - "EXP_DATE": "Date d'expiration", - "USERNAME": "Nom d'utilisateur", - "PASSWORD": "Mot de passe", - "SERVER": "Serveur", - "TIMEZONE": "Fuseau horaire", - "PATH": "Chemin", - "MESSAGE": "Message", - "PROTOCOL": "Protocole", - "NAME": "Nom" - }, - "TITLE": { - "USER_BOUQUET_EDITOR": "Éditeur de bouquet utilisateur" - }, - "MESSAGES": { - "INVALID_REGEXP": "Expression régulière invalide", - "INVALID_FILETYPE": "Type de fichier invalide", - "DOWNLOAD": { - "SUCCESS": "Téléchargement réussi", - "FAIL": "Échec du téléchargement!", - "PLAYLIST": { - "SUCCESS": "Liste de lecture téléchargée avec succès", - "FAIL": "Échec du téléchargement de la liste de lecture!" - }, - "SERVER_CONFIG": { - "FAIL": "Échec du téléchargement de la configuration du serveur!" - } - }, - "SAVE": { - "MAIN_CONFIG": { - "SUCCESS": "Configuration principale enregistrée!", - "FAIL": "Échec de l'enregistrement de la configuration principale!" - }, - "BOUQUET": { - "SUCCESS": "Bouquet enregistré avec succès", - "FAIL": "Échec de l'enregistrement du bouquet!" - }, - "USER": { - "SUCCESS": "Utilisateur enregistré avec succès", - "FAIL": "Échec de l'enregistrement de l'utilisateur!" - } - } - }, - "INFO": { - "RESTART_TO_APPLY_CHANGES": "Vous devez redémarrer pour appliquer les modifications.", - "SCHEDULE_EXAMPLE": "Exemple de programme", - "THREADS": "Threads: laissez-le à 0 si vous n'avez aucun problème. Augmentez-le si vous avez plusieurs fournisseurs. L'accès parallèle à votre fournisseur peut entraîner un ban.", - "WEB_ROOT": "Racine Web: Contient les fichiers de l'interface web. Ils sont normalement placés dans le même répertoire sous ./web" - } -} diff --git a/frontend/public/assets/i18n/it_common.json b/frontend/public/assets/i18n/it_common.json deleted file mode 100644 index 76a2a788c..000000000 --- a/frontend/public/assets/i18n/it_common.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "LABEL": { - "LIVE": "In diretta", - "VOD": "VOD", - "SERIES": "Serie", - "SAVE": "Salva", - "SHOW_SELECTED": "Mostra selezionati", - "SELECT_ALL": "Seleziona tutto", - "DESELECT_ALL": "Deseleziona tutto", - "SEARCH": "Cerca", - "CLEAR": "Cancella", - "REGEXP": "Espressione regolare", - "LOGOUT": "Esci", - "CONFIGURATION": "Configurazione", - "HOST": "Host", - "PORT": "Porta", - "WEB_ROOT": "Root web", - "THREADS": "Thread", - "WORKING_DIR": "Cartella di lavoro", - "BACKUP_DIR": "Cartella di backup", - "UPDATE_ON_BOOT": "Aggiorna all'avvio", - "WEB_UI": "Interfaccia web", - "SCHEDULES": "Orari", - "SANITIZE_SENSITIVE_INFO": "Sanitizza informazioni sensibili", - "ACTIVE_CLIENTS": "Client attivi", - "RESOURCE_REWRITE_DISABLE": "Disabilita riscrittura risorse", - "RECONNECT": "Riconnetti", - "BUFFER_ENABLED": "Buffer attivato", - "BUFFER_SIZE": "Dimensione buffer", - "ENABLED": "Abilitato", - "SIZE": "Dimensione", - "CACHE_DIR": "Cartella cache", - "NOTIFY_ON": "Notifica su", - "BOT_TOKEN": "Token bot", - "CHAT_IDS": "ID chat", - "URL": "URL", - "USER": "Utente", - "TOKEN": "Token", - "WEB_SEARCH_URL": "URL ricerca web", - "EXTENSIONS": "Estensioni", - "HEADERS": "Intestazioni", - "DIRECTORY": "Cartella", - "ORGANIZE_INTO_DIRECTORIES": "Organizza in cartelle", - "EPISODE_PATTERN": "Pattern episodio", - "API": "API", - "MAIN": "Principale", - "REVERSE_PROXY": "Proxy inverso", - "MESSAGING": "Messaggistica", - "VIDEO": "Video", - "EXAMPLES": "Esempi", - "UPDATE": "Aggiorna", - "START": "Avvia", - "CACHE": "Cache", - "STREAM": "Flusso", - "LOG": "Registro", - "CONFIG": "Configurazione", - "TELEGRAM": "Telegram", - "REST": "Rest", - "PUSHOVER": "Pushover", - "PROXY": "Proxy", - "REFRESH": "Ricarica", - "STATUS": "Stato", - "MAX_CON": "MaxCon", - "EXP_DATE": "Data di scadenza", - "USERNAME": "Nome utente", - "PASSWORD": "Password", - "SERVER": "Server", - "TIMEZONE": "Fuso orario", - "PATH": "Percorso", - "MESSAGE": "Messaggio", - "PROTOCOL": "Protocollo", - "NAME": "Nome" - }, - "TITLE": { - "USER_BOUQUET_EDITOR": "Editor bouquet utente" - }, - "MESSAGES": { - "INVALID_REGEXP": "Espressione regolare non valida", - "INVALID_FILETYPE": "Tipo di file non valido", - "DOWNLOAD": { - "SUCCESS": "Scaricato con successo", - "FAIL": "Download fallito!", - "PLAYLIST": { - "SUCCESS": "Playlist scaricata con successo", - "FAIL": "Download playlist fallito!" - }, - "SERVER_CONFIG": { - "FAIL": "Download configurazione server fallito!" - } - }, - "SAVE": { - "MAIN_CONFIG": { - "SUCCESS": "Configurazione principale salvata!", - "FAIL": "Salvataggio configurazione principale fallito!" - }, - "BOUQUET": { - "SUCCESS": "Bouquet salvato con successo", - "FAIL": "Salvataggio bouquet fallito!" - }, - "USER": { - "SUCCESS": "Utente salvato con successo", - "FAIL": "Salvataggio utente fallito!" - } - } - }, - "INFO": { - "RESTART_TO_APPLY_CHANGES": "È necessario riavviare per applicare le modifiche.", - "SCHEDULE_EXAMPLE": "Esempio di programma", - "THREADS": "Thread: lascia 0 se non hai problemi. Aumenta se hai più provider. L'accesso parallelo ai tuoi provider potrebbe causare un ban.", - "WEB_ROOT": "Root Web: Contiene i file dell'interfaccia web. Di solito vengono posizionati nella stessa cartella sotto ./web" - } -} diff --git a/frontend/public/assets/i18n/ru_common.json b/frontend/public/assets/i18n/ru_common.json deleted file mode 100644 index f321fe183..000000000 --- a/frontend/public/assets/i18n/ru_common.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "LABEL": { - "LIVE": "Прямой эфир", - "VOD": "VOD", - "SERIES": "Сериалы", - "SAVE": "Сохранить", - "SHOW_SELECTED": "Показать выбранное", - "SELECT_ALL": "Выбрать все", - "DESELECT_ALL": "Отменить выбор всех", - "SEARCH": "Поиск", - "CLEAR": "Очистить", - "REGEXP": "Регулярное выражение", - "LOGOUT": "Выйти", - "CONFIGURATION": "Конфигурация", - "HOST": "Хост", - "PORT": "Порт", - "WEB_ROOT": "Корень веба", - "THREADS": "Потоки", - "WORKING_DIR": "Рабочий каталог", - "BACKUP_DIR": "Каталог резервных копий", - "UPDATE_ON_BOOT": "Обновить при загрузке", - "WEB_UI": "Веб-интерфейс", - "SCHEDULES": "Расписания", - "SANITIZE_SENSITIVE_INFO": "Очистить чувствительную информацию", - "ACTIVE_CLIENTS": "Активные клиенты", - "RESOURCE_REWRITE_DISABLE": "Отключить переписывание ресурсов", - "RECONNECT": "Переподключить", - "BUFFER_ENABLED": "Буфер включен", - "BUFFER_SIZE": "Размер буфера", - "ENABLED": "Включено", - "SIZE": "Размер", - "CACHE_DIR": "Каталог кэша", - "NOTIFY_ON": "Уведомить о", - "BOT_TOKEN": "Токен бота", - "CHAT_IDS": "ID чатов", - "URL": "URL", - "USER": "Пользователь", - "TOKEN": "Токен", - "WEB_SEARCH_URL": "URL веб-поиска", - "EXTENSIONS": "Расширения", - "HEADERS": "Заголовки", - "DIRECTORY": "Каталог", - "ORGANIZE_INTO_DIRECTORIES": "Организовать в каталоги", - "EPISODE_PATTERN": "Шаблон эпизодов", - "API": "API", - "MAIN": "Основное", - "REVERSE_PROXY": "Обратный прокси", - "MESSAGING": "Сообщения", - "VIDEO": "Видео", - "EXAMPLES": "Примеры", - "UPDATE": "Обновить", - "START": "Старт", - "CACHE": "Кэш", - "STREAM": "Поток", - "LOG": "Лог", - "CONFIG": "Конфигурация", - "TELEGRAM": "Телеграм", - "REST": "Rest", - "PUSHOVER": "Pushover", - "PROXY": "Прокси", - "REFRESH": "Обновить", - "STATUS": "Статус", - "MAX_CON": "Макс. соединений", - "EXP_DATE": "Дата истечения", - "USERNAME": "Имя пользователя", - "PASSWORD": "Пароль", - "SERVER": "Сервер", - "TIMEZONE": "Часовой пояс", - "PATH": "Путь", - "MESSAGE": "Сообщение", - "PROTOCOL": "Протокол", - "NAME": "Имя" - }, - "TITLE": { - "USER_BOUQUET_EDITOR": "Редактор букета пользователя" - }, - "MESSAGES": { - "INVALID_REGEXP": "Неверное регулярное выражение", - "INVALID_FILETYPE": "Неверный тип файла", - "DOWNLOAD": { - "SUCCESS": "Загрузка успешна", - "FAIL": "Ошибка загрузки!", - "PLAYLIST": { - "SUCCESS": "Плейлист успешно загружен", - "FAIL": "Ошибка загрузки плейлиста!" - }, - "SERVER_CONFIG": { - "FAIL": "Не удалось загрузить конфигурацию сервера!" - } - }, - "SAVE": { - "MAIN_CONFIG": { - "SUCCESS": "Основная конфигурация сохранена!", - "FAIL": "Не удалось сохранить основную конфигурацию!" - }, - "BOUQUET": { - "SUCCESS": "Букет успешно сохранен", - "FAIL": "Не удалось сохранить букет!" - }, - "USER": { - "SUCCESS": "Пользователь успешно сохранен", - "FAIL": "Не удалось сохранить пользователя!" - } - } - }, - "INFO": { - "RESTART_TO_APPLY_CHANGES": "Необходимо перезагрузить систему, чтобы применить изменения.", - "SCHEDULE_EXAMPLE": "Пример расписания", - "THREADS": "Потоки: оставьте 0, если нет проблем. Увеличьте, если у вас несколько провайдеров. Одновременный доступ к нескольким провайдерам может привести к блокировке.", - "WEB_ROOT": "Корень веба: содержит файлы веб-интерфейса. Обычно они размещаются в той же директории ./web" - } -} diff --git a/frontend/public/assets/i18n/tr_common.json b/frontend/public/assets/i18n/tr_common.json deleted file mode 100644 index 0033ccda2..000000000 --- a/frontend/public/assets/i18n/tr_common.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "LABEL": { - "LIVE": "Canlı", - "VOD": "Video", - "SERIES": "Diziler", - "SAVE": "Kaydet", - "SUBMIT": "Onayla", - "OK": "Tamam", - "CANCEL": "İptal", - "SHOW_SELECTED": "Seçilenleri Göster", - "SELECT_ALL": "Tümünü Seç", - "DESELECT_ALL": "Tümünü Seçimden Çıkar", - "SEARCH": "Ara", - "CLEAR": "Sil", - "REGEXP": "RegExp", - "LOGOUT": "Çıkış Yap", - "CONFIGURATION": "Yapılandırma", - "HOST": "Sunucu", - "PORT": "Port", - "WEB_ROOT": "Web Klasörü", - "THREADS": "İşlemciler", - "WORKING_DIR": "Çalışma Klasörü", - "BACKUP_DIR": "Yedekleme Klasörü", - "UPDATE_ON_BOOT": "Başlangıçta Güncelle", - "WEB_UI": "Web UI", - "SCHEDULES": "Programlar", - "SANITIZE_SENSITIVE_INFO": "Hassas Bilgileri Temizle", - "ACTIVE_CLIENTS": "Aktif Kullanıcılar", - "RESOURCE_REWRITE_DISABLE": "Url Yönlendirme Devre Dışı", - "RECONNECT": "Yeniden Bağlan", - "BUFFER_ENABLED": "Arabellek Aktif", - "BUFFER_SIZE": "Arabellek Boyutu", - "ENABLED": "Aktif", - "SIZE": "Boyut", - "CACHE_DIR": "Önbellek Dizin", - "NOTIFY_ON": "Bildirimler", - "BOT_TOKEN": "Bot Token'i", - "CHAT_IDS": "Chat ID'leri", - "URL": "URL", - "USER": "Kullanıcı", - "TOKEN": "Token", - "WEB_SEARCH_URL": "Web Arama URL'si", - "EXTENSIONS": "Eklentiler", - "HEADERS": "Başlıklar", - "DIRECTORY": "Klasör", - "ORGANIZE_INTO_DIRECTORIES": "Klasörlere ayır", - "EPISODE_PATTERN": "Bölüm Pattern'i", - "API": "API", - "MAIN": "Ana", - "REVERSE_PROXY": "Ters Proxy", - "MESSAGING": "Mesajlaşma", - "VIDEO": "Video", - "EXAMPLES": "Örnekler", - "UPDATE": "Güncelle", - "START": "Başlat", - "CACHE": "Önbellek", - "STREAM": "Yayın", - "LOG": "Kayıt", - "CONFIG": "Yapılandırma", - "TELEGRAM": "Telegram", - "REST": "Rest", - "PUSHOVER": "Pushover", - "PROXY": "Proxy", - "REFRESH": "Yenile", - "STATUS": "Durum", - "MAX_CON": "Max. Bağlantı", - "EXP_DATE": "Son Kullanma Tarihi", - "USERNAME": "Kullanıcı Adı", - "PASSWORD": "Şifre", - "SERVER": "Sunucu", - "TIMEZONE": "Zaman Dilimi", - "PATH": "Yol", - "MESSAGE": "Mesaj", - "PROTOCOL": "Protokol", - "NAME": "Ad" - }, - "TITLE": { - "USER_BOUQUET_EDITOR": "Kullanıcı Buket Editörü" - }, - "MESSAGES": { - "INVALID_REGEXP": "Geçersiz regexp", - "INVALID_FILETYPE": "Geçersiz dosya türü", - "DOWNLOAD": { - "SUCCESS": "Başarıyla indirildi", - "FAIL": "İndirme başarısız!", - "PLAYLIST": { - "SUCCESS": "Liste başarıyla indirildi", - "FAIL": "Liste indirilemedi!" - }, - "SERVER_CONFIG": { - "FAIL": "Sunucu yapılandırması indirilemedi!" - } - }, - "SAVE": { - "MAIN_CONFIG": { - "SUCCESS": "Ana yapılandırma kaydedildi!", - "FAIL": "Ana yapılandırma kaydedilemedi!" - }, - "BOUQUET": { - "SUCCESS": "Buket başarıyla kaydedildi", - "FAIL": "Buket kaydedilemedi!" - }, - "USER": { - "SUCCESS": "Kullanıcı başarıyla kaydedildi", - "FAIL": "Kullanıcı kaydedilemedi!" - } - } - }, - "INFO": { - "RESTART_TO_APPLY_CHANGES": "Değişiklikleri uygulamak için yeniden başlatmanız gerekmektedir.", - "SCHEDULE_EXAMPLE": "Program örneği", - "THREADS": "İşlemciler: Eğer bir sorununuz yoksa 0 olarak bırakın. Birden fazla sağlayıcı kullanıyorsanız artırabilirsiniz. Sağlayıcınıza paralel erişim, yasaklanmanıza neden olabilir.", - "WEB_ROOT": "Web klassörü: Web-UI dosyalarını içerir. Genellikle ./web altında aynı dizine yerleştirilir." - } -} diff --git a/webui/public/assets/icons.json b/frontend/public/assets/icons.json similarity index 100% rename from webui/public/assets/icons.json rename to frontend/public/assets/icons.json diff --git a/frontend/public/config.json b/frontend/public/config.json index 390115357..138d5b0b7 100644 --- a/frontend/public/config.json +++ b/frontend/public/config.json @@ -1,9 +1,12 @@ { - "tab_title": "tuliprox", - "app_title": "tuliprox", - "app_logo": "assets/tuliprox-logo.svg", + "tabTitle": "tuliprox", + "appLogo": "/assets/tuliprox-logo.svg", "api": { "apiUrl": "/api/v1/", "authUrl": "/auth/" - } + }, + "discord": "https://discord.gg/gkzCmWw9Tf", + "documentation": "https://github.com/euzu/tuliprox/blob/master/README.md", + "wsUrl" : "/ws", + "protocolVersion": 1 } \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html deleted file mode 100644 index fcc64ac04..000000000 --- a/frontend/public/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - tuliprox - - - -
- - - diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json deleted file mode 100644 index 665f6df82..000000000 --- a/frontend/public/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "short_name": "tuliprox", - "name": "tuliprox", - "icons": [ - { - "src": "assets/favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt deleted file mode 100644 index e9e57dc4d..000000000 --- a/frontend/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/webui/scss/_default.scss b/frontend/scss/_default.scss similarity index 100% rename from webui/scss/_default.scss rename to frontend/scss/_default.scss diff --git a/webui/scss/_size.scss b/frontend/scss/_size.scss similarity index 100% rename from webui/scss/_size.scss rename to frontend/scss/_size.scss diff --git a/webui/scss/_theme.scss b/frontend/scss/_theme.scss similarity index 100% rename from webui/scss/_theme.scss rename to frontend/scss/_theme.scss diff --git a/webui/scss/app/_app.scss b/frontend/scss/app/_app.scss similarity index 100% rename from webui/scss/app/_app.scss rename to frontend/scss/app/_app.scss diff --git a/webui/scss/app/_app_header.scss b/frontend/scss/app/_app_header.scss similarity index 100% rename from webui/scss/app/_app_header.scss rename to frontend/scss/app/_app_header.scss diff --git a/webui/scss/app/_component.scss b/frontend/scss/app/_component.scss similarity index 100% rename from webui/scss/app/_component.scss rename to frontend/scss/app/_component.scss diff --git a/webui/scss/app/components/_breadcrumbs.scss b/frontend/scss/app/components/_breadcrumbs.scss similarity index 100% rename from webui/scss/app/components/_breadcrumbs.scss rename to frontend/scss/app/components/_breadcrumbs.scss diff --git a/webui/scss/app/components/_card.scss b/frontend/scss/app/components/_card.scss similarity index 100% rename from webui/scss/app/components/_card.scss rename to frontend/scss/app/components/_card.scss diff --git a/webui/scss/app/components/_chip.scss b/frontend/scss/app/components/_chip.scss similarity index 100% rename from webui/scss/app/components/_chip.scss rename to frontend/scss/app/components/_chip.scss diff --git a/webui/scss/app/components/_collapse_panel.scss b/frontend/scss/app/components/_collapse_panel.scss similarity index 100% rename from webui/scss/app/components/_collapse_panel.scss rename to frontend/scss/app/components/_collapse_panel.scss diff --git a/webui/scss/app/components/_custom_dialog.scss b/frontend/scss/app/components/_custom_dialog.scss similarity index 97% rename from webui/scss/app/components/_custom_dialog.scss rename to frontend/scss/app/components/_custom_dialog.scss index 4c8cb8c93..b852b66e1 100644 --- a/webui/scss/app/components/_custom_dialog.scss +++ b/frontend/scss/app/components/_custom_dialog.scss @@ -73,6 +73,7 @@ display: flex; justify-content: flex-end; border-top: 1px solid var(--border-color); + padding-top: var(--padding-small); gap: var(--gap-default); &-left { diff --git a/webui/scss/app/components/_dialog.scss b/frontend/scss/app/components/_dialog.scss similarity index 100% rename from webui/scss/app/components/_dialog.scss rename to frontend/scss/app/components/_dialog.scss diff --git a/webui/scss/app/components/_drop_down_icon_button.scss b/frontend/scss/app/components/_drop_down_icon_button.scss similarity index 100% rename from webui/scss/app/components/_drop_down_icon_button.scss rename to frontend/scss/app/components/_drop_down_icon_button.scss diff --git a/webui/scss/app/components/_filter.scss b/frontend/scss/app/components/_filter.scss similarity index 100% rename from webui/scss/app/components/_filter.scss rename to frontend/scss/app/components/_filter.scss diff --git a/webui/scss/app/components/_hide_content.scss b/frontend/scss/app/components/_hide_content.scss similarity index 100% rename from webui/scss/app/components/_hide_content.scss rename to frontend/scss/app/components/_hide_content.scss diff --git a/webui/scss/app/components/_icon_button.scss b/frontend/scss/app/components/_icon_button.scss similarity index 100% rename from webui/scss/app/components/_icon_button.scss rename to frontend/scss/app/components/_icon_button.scss diff --git a/webui/scss/app/components/_input.scss b/frontend/scss/app/components/_input.scss similarity index 100% rename from webui/scss/app/components/_input.scss rename to frontend/scss/app/components/_input.scss diff --git a/webui/scss/app/components/_list_view.scss b/frontend/scss/app/components/_list_view.scss similarity index 100% rename from webui/scss/app/components/_list_view.scss rename to frontend/scss/app/components/_list_view.scss diff --git a/webui/scss/app/components/_loading_indicator.scss b/frontend/scss/app/components/_loading_indicator.scss similarity index 100% rename from webui/scss/app/components/_loading_indicator.scss rename to frontend/scss/app/components/_loading_indicator.scss diff --git a/webui/scss/app/components/_login.scss b/frontend/scss/app/components/_login.scss similarity index 100% rename from webui/scss/app/components/_login.scss rename to frontend/scss/app/components/_login.scss diff --git a/webui/scss/app/components/_mapper_script.scss b/frontend/scss/app/components/_mapper_script.scss similarity index 97% rename from webui/scss/app/components/_mapper_script.scss rename to frontend/scss/app/components/_mapper_script.scss index 02ec7d246..f6153b803 100644 --- a/webui/scss/app/components/_mapper_script.scss +++ b/frontend/scss/app/components/_mapper_script.scss @@ -47,6 +47,10 @@ } + .reserved { + color: #da2ee0; + } + //.binary_op-wrapper{ // white-space: pre; //} diff --git a/webui/scss/app/components/_menu_item.scss b/frontend/scss/app/components/_menu_item.scss similarity index 100% rename from webui/scss/app/components/_menu_item.scss rename to frontend/scss/app/components/_menu_item.scss diff --git a/webui/scss/app/components/_no_content.scss b/frontend/scss/app/components/_no_content.scss similarity index 100% rename from webui/scss/app/components/_no_content.scss rename to frontend/scss/app/components/_no_content.scss diff --git a/webui/scss/app/components/_panel.scss b/frontend/scss/app/components/_panel.scss similarity index 100% rename from webui/scss/app/components/_panel.scss rename to frontend/scss/app/components/_panel.scss diff --git a/webui/scss/app/components/_popup_menu.scss b/frontend/scss/app/components/_popup_menu.scss similarity index 100% rename from webui/scss/app/components/_popup_menu.scss rename to frontend/scss/app/components/_popup_menu.scss diff --git a/webui/scss/app/components/_preferences.scss b/frontend/scss/app/components/_preferences.scss similarity index 100% rename from webui/scss/app/components/_preferences.scss rename to frontend/scss/app/components/_preferences.scss diff --git a/webui/scss/app/components/_radio_button_group.scss b/frontend/scss/app/components/_radio_button_group.scss similarity index 100% rename from webui/scss/app/components/_radio_button_group.scss rename to frontend/scss/app/components/_radio_button_group.scss diff --git a/webui/scss/app/components/_reveal_content.scss b/frontend/scss/app/components/_reveal_content.scss similarity index 100% rename from webui/scss/app/components/_reveal_content.scss rename to frontend/scss/app/components/_reveal_content.scss diff --git a/webui/scss/app/components/_search.scss b/frontend/scss/app/components/_search.scss similarity index 100% rename from webui/scss/app/components/_search.scss rename to frontend/scss/app/components/_search.scss diff --git a/webui/scss/app/components/_sidebar.scss b/frontend/scss/app/components/_sidebar.scss similarity index 100% rename from webui/scss/app/components/_sidebar.scss rename to frontend/scss/app/components/_sidebar.scss diff --git a/webui/scss/app/components/_table.scss b/frontend/scss/app/components/_table.scss similarity index 100% rename from webui/scss/app/components/_table.scss rename to frontend/scss/app/components/_table.scss diff --git a/webui/scss/app/components/_tag_list.scss b/frontend/scss/app/components/_tag_list.scss similarity index 100% rename from webui/scss/app/components/_tag_list.scss rename to frontend/scss/app/components/_tag_list.scss diff --git a/webui/scss/app/components/_text_button.scss b/frontend/scss/app/components/_text_button.scss similarity index 100% rename from webui/scss/app/components/_text_button.scss rename to frontend/scss/app/components/_text_button.scss diff --git a/webui/scss/app/components/_toastr.scss b/frontend/scss/app/components/_toastr.scss similarity index 100% rename from webui/scss/app/components/_toastr.scss rename to frontend/scss/app/components/_toastr.scss diff --git a/webui/scss/app/components/_toggle_switch.scss b/frontend/scss/app/components/_toggle_switch.scss similarity index 100% rename from webui/scss/app/components/_toggle_switch.scss rename to frontend/scss/app/components/_toggle_switch.scss diff --git a/webui/scss/app/components/dashboard/_action_card.scss b/frontend/scss/app/components/dashboard/_action_card.scss similarity index 100% rename from webui/scss/app/components/dashboard/_action_card.scss rename to frontend/scss/app/components/dashboard/_action_card.scss diff --git a/webui/scss/app/components/dashboard/_dashboard_view.scss b/frontend/scss/app/components/dashboard/_dashboard_view.scss similarity index 100% rename from webui/scss/app/components/dashboard/_dashboard_view.scss rename to frontend/scss/app/components/dashboard/_dashboard_view.scss diff --git a/webui/scss/app/components/dashboard/_stats_view.scss b/frontend/scss/app/components/dashboard/_stats_view.scss similarity index 100% rename from webui/scss/app/components/dashboard/_stats_view.scss rename to frontend/scss/app/components/dashboard/_stats_view.scss diff --git a/webui/scss/app/components/dashboard/_status_card.scss b/frontend/scss/app/components/dashboard/_status_card.scss similarity index 100% rename from webui/scss/app/components/dashboard/_status_card.scss rename to frontend/scss/app/components/dashboard/_status_card.scss diff --git a/webui/scss/app/components/playlist/_playlist_editor_view.scss b/frontend/scss/app/components/playlist/_playlist_editor_view.scss similarity index 100% rename from webui/scss/app/components/playlist/_playlist_editor_view.scss rename to frontend/scss/app/components/playlist/_playlist_editor_view.scss diff --git a/webui/scss/app/components/playlist/_playlist_explorer.scss b/frontend/scss/app/components/playlist/_playlist_explorer.scss similarity index 100% rename from webui/scss/app/components/playlist/_playlist_explorer.scss rename to frontend/scss/app/components/playlist/_playlist_explorer.scss diff --git a/webui/scss/app/components/playlist/_playlist_explorer_view.scss b/frontend/scss/app/components/playlist/_playlist_explorer_view.scss similarity index 100% rename from webui/scss/app/components/playlist/_playlist_explorer_view.scss rename to frontend/scss/app/components/playlist/_playlist_explorer_view.scss diff --git a/frontend/scss/app/components/playlist/_playlist_mappings.scss b/frontend/scss/app/components/playlist/_playlist_mappings.scss new file mode 100644 index 000000000..c351f2126 --- /dev/null +++ b/frontend/scss/app/components/playlist/_playlist_mappings.scss @@ -0,0 +1,59 @@ +@use '../../../../scss/size'; + +.tp__playlist-mappings { + display: flex; + flex-flow: column; + justify-content: center; + align-items: flex-start; + box-sizing: border-box; + overflow: hidden; + + &__mapping { + display: flex; + flex-flow: column; + gap: var(--gap-default); + justify-content: center; + box-sizing: border-box; + overflow: hidden; + + &-section { + display: flex; + flex-flow: row ; + align-items: center; + box-sizing: border-box; + gap: var(--gap-small); + overflow: hidden; + } + + &-mapper { + display: flex; + grid-template-columns: 1fr 1fr; + + @media (max-width: size.$dashboard-mobile-breakpoint) { + grid-template-columns: 1fr; + } + + gap: var(--gap-default); + box-sizing: border-box; + overflow: auto; + + &-content { + display: flex; + flex-flow: column; + gap: var(--gap-default); + justify-content: center; + align-items: flex-start; + box-sizing: border-box; + } + } + } + + label { + font-weight: bold; + color: var(--modest-text-color); + + &:after { + content: ':' + } + } +} \ No newline at end of file diff --git a/webui/scss/app/components/playlist/_playlist_processing.scss b/frontend/scss/app/components/playlist/_playlist_processing.scss similarity index 100% rename from webui/scss/app/components/playlist/_playlist_processing.scss rename to frontend/scss/app/components/playlist/_playlist_processing.scss diff --git a/webui/scss/app/components/playlist/_playlist_update_view.scss b/frontend/scss/app/components/playlist/_playlist_update_view.scss similarity index 97% rename from webui/scss/app/components/playlist/_playlist_update_view.scss rename to frontend/scss/app/components/playlist/_playlist_update_view.scss index 1920451fa..1d363f055 100644 --- a/webui/scss/app/components/playlist/_playlist_update_view.scss +++ b/frontend/scss/app/components/playlist/_playlist_update_view.scss @@ -1,7 +1,7 @@ .tp__playlist-update-view { display: flex; flex-flow: column; - width: 100%; + width: 80vw; gap: var(--gap-default); padding: var(--padding-default) 0; box-sizing: border-box; @@ -17,6 +17,7 @@ &__body { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--gap-default); box-sizing: border-box; overflow-y: auto; diff --git a/webui/scss/app/components/playlist/_source_selector.scss b/frontend/scss/app/components/playlist/_source_selector.scss similarity index 100% rename from webui/scss/app/components/playlist/_source_selector.scss rename to frontend/scss/app/components/playlist/_source_selector.scss diff --git a/webui/scss/app/components/playlist/assistant/_playlist_assistant.scss b/frontend/scss/app/components/playlist/assistant/_playlist_assistant.scss similarity index 100% rename from webui/scss/app/components/playlist/assistant/_playlist_assistant.scss rename to frontend/scss/app/components/playlist/assistant/_playlist_assistant.scss diff --git a/webui/scss/app/components/playlist/input/_input_table.scss b/frontend/scss/app/components/playlist/input/_input_table.scss similarity index 100% rename from webui/scss/app/components/playlist/input/_input_table.scss rename to frontend/scss/app/components/playlist/input/_input_table.scss diff --git a/webui/scss/app/components/playlist/input/_input_type.scss b/frontend/scss/app/components/playlist/input/_input_type.scss similarity index 100% rename from webui/scss/app/components/playlist/input/_input_type.scss rename to frontend/scss/app/components/playlist/input/_input_type.scss diff --git a/webui/scss/app/components/playlist/target/_hdhomerun_output.scss b/frontend/scss/app/components/playlist/target/_hdhomerun_output.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_hdhomerun_output.scss rename to frontend/scss/app/components/playlist/target/_hdhomerun_output.scss diff --git a/webui/scss/app/components/playlist/target/_m3u_output.scss b/frontend/scss/app/components/playlist/target/_m3u_output.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_m3u_output.scss rename to frontend/scss/app/components/playlist/target/_m3u_output.scss diff --git a/webui/scss/app/components/playlist/target/_strm_output.scss b/frontend/scss/app/components/playlist/target/_strm_output.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_strm_output.scss rename to frontend/scss/app/components/playlist/target/_strm_output.scss diff --git a/webui/scss/app/components/playlist/target/_target_common.scss b/frontend/scss/app/components/playlist/target/_target_common.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_target_common.scss rename to frontend/scss/app/components/playlist/target/_target_common.scss diff --git a/webui/scss/app/components/playlist/target/_target_options.scss b/frontend/scss/app/components/playlist/target/_target_options.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_target_options.scss rename to frontend/scss/app/components/playlist/target/_target_options.scss diff --git a/webui/scss/app/components/playlist/target/_target_output.scss b/frontend/scss/app/components/playlist/target/_target_output.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_target_output.scss rename to frontend/scss/app/components/playlist/target/_target_output.scss diff --git a/webui/scss/app/components/playlist/target/_target_rename.scss b/frontend/scss/app/components/playlist/target/_target_rename.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_target_rename.scss rename to frontend/scss/app/components/playlist/target/_target_rename.scss diff --git a/webui/scss/app/components/playlist/target/_target_sort.scss b/frontend/scss/app/components/playlist/target/_target_sort.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_target_sort.scss rename to frontend/scss/app/components/playlist/target/_target_sort.scss diff --git a/webui/scss/app/components/playlist/target/_target_table.scss b/frontend/scss/app/components/playlist/target/_target_table.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_target_table.scss rename to frontend/scss/app/components/playlist/target/_target_table.scss diff --git a/webui/scss/app/components/playlist/target/_target_watch.scss b/frontend/scss/app/components/playlist/target/_target_watch.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_target_watch.scss rename to frontend/scss/app/components/playlist/target/_target_watch.scss diff --git a/webui/scss/app/components/playlist/target/_xtream_output.scss b/frontend/scss/app/components/playlist/target/_xtream_output.scss similarity index 100% rename from webui/scss/app/components/playlist/target/_xtream_output.scss rename to frontend/scss/app/components/playlist/target/_xtream_output.scss diff --git a/webui/scss/app/components/userlist/_max_connections.scss b/frontend/scss/app/components/userlist/_max_connections.scss similarity index 100% rename from webui/scss/app/components/userlist/_max_connections.scss rename to frontend/scss/app/components/userlist/_max_connections.scss diff --git a/webui/scss/app/components/userlist/_proxy_type.scss b/frontend/scss/app/components/userlist/_proxy_type.scss similarity index 100% rename from webui/scss/app/components/userlist/_proxy_type.scss rename to frontend/scss/app/components/userlist/_proxy_type.scss diff --git a/webui/scss/app/components/userlist/_user_state.scss b/frontend/scss/app/components/userlist/_user_state.scss similarity index 100% rename from webui/scss/app/components/userlist/_user_state.scss rename to frontend/scss/app/components/userlist/_user_state.scss diff --git a/webui/scss/app/components/userlist/_userlist_view.scss b/frontend/scss/app/components/userlist/_userlist_view.scss similarity index 100% rename from webui/scss/app/components/userlist/_userlist_view.scss rename to frontend/scss/app/components/userlist/_userlist_view.scss diff --git a/webui/scss/main.scss b/frontend/scss/main.scss similarity index 100% rename from webui/scss/main.scss rename to frontend/scss/main.scss diff --git a/frontend/src/api/api-service.ts b/frontend/src/api/api-service.ts deleted file mode 100644 index 736764d4c..000000000 --- a/frontend/src/api/api-service.ts +++ /dev/null @@ -1,153 +0,0 @@ -import {Observable} from "rxjs"; -import axios from "axios"; -import ServiceContext from "../service/service-context"; - -const HEADER_CONTENT_TYPE = 'Content-Type'; -const HEADER_LANGUAGE = 'X-Language'; -const HEADER_ACCEPT = 'Accept'; -const HEADER_AUTHORIZATION = 'Authorization'; - -export default interface ApiService { - get(query: string, url?: string): Observable; - - post(query: string, payload: any, url?: string): Observable; - - put(query: string, payload: any, url?: string): Observable; - - delete(query: string, url?: string): Observable; - - postFile(query: string, fileName: string, file: any, url?: string): Observable; - downloadFile(query: string, payload: any, url?: string): Observable; -} - -export class DefaultApiService implements ApiService { - - private readonly DEFAULT_ERROR = {'origin': 'server', 'message': 'Server error'}; - - private static getLanguage(): string { - return "en"; - } - - private prepareError(err: any): any { - return err || this.DEFAULT_ERROR; - } - - private static getOption(options: any, key: any, defaultValue: any): string { - if (options) { - if (Object.prototype.hasOwnProperty.call(options, key)) { - return options[key]; - } - } - return defaultValue; - } - - protected getHeaders(options?: unknown): any { - const headers: any = {}; - const language = DefaultApiService.getLanguage(); - if (language) { - headers[HEADER_LANGUAGE] = language; - } - const value = DefaultApiService.getOption(options, HEADER_CONTENT_TYPE, 'application/json; charset=utf-8'); - if (value) { - headers[HEADER_CONTENT_TYPE] = value; - } - headers[HEADER_ACCEPT] = 'application/json'; - const token = ServiceContext.auth().getToken(); - if (token) { - headers[HEADER_AUTHORIZATION] = 'Bearer ' + token; - } - return headers; - } - - protected getUrl(query: string, url?: string) { - const apiBaseUrl = ServiceContext.config().getUiConfig().api.apiUrl; - return (url ? url : apiBaseUrl) + query; - } - - get(query: string, url?: string): Observable { - return new Observable((observer) => { - axios.get(this.getUrl(query, url), {headers: this.getHeaders()}) - .then((response) => { - observer.next(response.data); - observer.complete(); - }) - .catch((error) => observer.error(this.prepareError(error))); - }); - } - - post(query: string, payload: any, url?: string): Observable { - return new Observable((observer) => { - axios.post(this.getUrl(query, url), payload, {headers: this.getHeaders()}) - .then((response) => { - observer.next(response.data); - observer.complete(); - }) - .catch((error) => observer.error(this.prepareError(error))); - }); - } - - put(query: string, payload: any, url?: string): Observable { - return new Observable((observer) => { - axios.put(this.getUrl(query, url), payload, {headers: this.getHeaders()}) - .then((response) => { - observer.next(response.data); - observer.complete(); - }) - .catch((error) => observer.error(this.prepareError(error))); - }); - } - - delete(query: string, url?: string): Observable { - return new Observable((observer) => { - axios.delete(this.getUrl(query, url), {headers: this.getHeaders()}) - .then((response) => { - observer.next(response.data); - observer.complete(); - }) - .catch((error) => observer.error(this.prepareError(error))); - }); - } - - postFile(query: string, fileName: string, file: any, url?: string): Observable { - const fd = new FormData(); - fd.append("fileName", fileName); - fd.append("file", file, fileName); - - return new Observable((observer) => { - axios.post(this.getUrl(query, url), fd, {headers: this.getHeaders({[HEADER_CONTENT_TYPE]: undefined})}) - .then((response) => { - observer.next(response.data); - observer.complete(); - }) - .catch((error) => observer.error(this.prepareError(error))); - }); - } - - downloadFile(query: string, payload: any, url?: string): Observable { - return this.post(query, payload, url); - // return new Observable((observer) => { - // - // axios.post(this.getUrl(query, url), payload, {responseType: 'blob', headers: this.getHeaders()}) - // .then((response) => { - // const disposition = response.headers['content-disposition']; - // let filename = disposition.split(/;(.+)/)[1].split(/=(.+)/)[1]; - // if (filename.toLowerCase().startsWith("utf-8''")) - // filename = decodeURIComponent(filename.replace("utf-8''", '')); - // else - // filename = filename.replace(/['"]/g, ''); - // return {filename, data: response.data}; - // }).then((params: { filename: string, data: any }) => { - // const url = window.URL.createObjectURL(params.data); - // const a = document.createElement('a'); - // a.href = url; - // a.download = params.filename; - // document.body.appendChild(a); // append the element to the dom - // a.click(); - // a.remove(); // afterwards, remove the element - // observer.next(true as any); - // observer.complete(); - // }).catch((error) => observer.error(this.prepareError(error))); - // }); - } - -} diff --git a/frontend/src/api/auth-api-service.ts b/frontend/src/api/auth-api-service.ts deleted file mode 100644 index 6e2b286f2..000000000 --- a/frontend/src/api/auth-api-service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import ApiService, {DefaultApiService} from "./api-service"; -import {Observable} from "rxjs"; -import ServiceContext from "../service/service-context"; - -type TokenResponse = { token: string }; - -export default interface AuthApiService extends ApiService { - authenticate(username: string, password: string): Observable; - - refresh(): Observable -} - -export class DefaultAuthApiService extends DefaultApiService implements AuthApiService { - constructor() { - super(); - } - - private getAuthUrl() : string { - let authUrl = ServiceContext.config().getUiConfig().api.authUrl; - if (authUrl.endsWith("/")) { - authUrl = authUrl.slice(0, -1); - } - return authUrl; - } - - authenticate(username: string, password: string): Observable<{ token: string }> { - const authBaseUrl = this.getAuthUrl(); - return this.post('/token', {username, password}, authBaseUrl); - } - - refresh(): Observable { - const authBaseUrl = this.getAuthUrl(); - return this.post('/refresh', {}, authBaseUrl); - } -} diff --git a/frontend/src/api/config-api-service.ts b/frontend/src/api/config-api-service.ts deleted file mode 100644 index 585315843..000000000 --- a/frontend/src/api/config-api-service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import ApiService, {DefaultApiService} from "./api-service"; -import {Observable} from "rxjs"; -import ServerConfig, {ApiProxyServerInfo, ServerMainConfig, TargetUser} from "../model/server-config"; - -const SERVER_CONFIG_API_PATH = 'config'; -const SERVER_CONFIG_MAIN_API_PATH = 'config/main'; -const SERVER_CONFIG_TARGET_USER_API_PATH = 'config/user'; -const SERVER_CONFIG_SERVER_INFO_API_PATH = 'config/apiproxy'; - -export default interface ConfigApiService extends ApiService { - getServerConfig(): Observable; - - saveMainConfig(config: ServerMainConfig): Observable; - - saveTargetUser(targetUser: TargetUser[]): Observable; - - saveApiProxyConfig(serverInfo: ApiProxyServerInfo[]): Observable; -} - -export class DefaultConfigApiService extends DefaultApiService implements ConfigApiService { - getServerConfig(): Observable { - return this.get(SERVER_CONFIG_API_PATH); - } - - saveMainConfig(config: ServerMainConfig): Observable { - return this.post(SERVER_CONFIG_MAIN_API_PATH, config); - } - - saveTargetUser(targetUser: TargetUser[]): Observable { - return this.post(SERVER_CONFIG_TARGET_USER_API_PATH, targetUser); - } - - saveApiProxyConfig(serverInfo: ApiProxyServerInfo[]): Observable { - return this.post(SERVER_CONFIG_SERVER_INFO_API_PATH, serverInfo); - } - -} diff --git a/frontend/src/api/file-api-service.ts b/frontend/src/api/file-api-service.ts deleted file mode 100644 index f7fc91148..000000000 --- a/frontend/src/api/file-api-service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import ApiService, {DefaultApiService} from "./api-service"; -import {Observable, throwError} from "rxjs"; -import {DownloadInfo, FileDownloadInfo, FileDownloadRequest} from "../model/file-download"; -import {first} from "rxjs/operators"; - -//const FILE_API_PATH = 'file'; -const FILE_DOWNLOAD_API_PATH = 'file/download'; -const FILE_DOWNLOAD_INFO_API_PATH = FILE_DOWNLOAD_API_PATH + '/info'; - -export default interface FileApiService extends ApiService { - download(req: FileDownloadRequest): Observable; - - getDownloadInfo(): Observable; -} - -export class DefaultFileApiService extends DefaultApiService implements FileApiService { - download(req: FileDownloadRequest): Observable { - if (req.url) { - return this.post(FILE_DOWNLOAD_API_PATH, req); - } - return throwError(() => new Error('Invalid arguments')); - } - - getDownloadInfo(): Observable { - return new Observable((observer) => { - const fetch_info = () => { - this.get(FILE_DOWNLOAD_INFO_API_PATH).pipe(first()).subscribe({ - next: (info: DownloadInfo) => { - if (info.completed) { - observer.next(info); - observer.complete(); - } else { - observer.next(info); - setTimeout(() => fetch_info(), 1000); - } - }, - error: (err) => observer.error(err), - }); - } - fetch_info(); - }); - } -} diff --git a/frontend/src/api/playlist-api-service.ts b/frontend/src/api/playlist-api-service.ts deleted file mode 100644 index 2fc7363e3..000000000 --- a/frontend/src/api/playlist-api-service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import ApiService, {DefaultApiService} from "./api-service"; -import {Observable, throwError} from "rxjs"; -import {PlaylistRequest} from "../model/playlist-request"; -import {PlaylistCategories, PlaylistItem} from "../model/playlist"; - -const PLAYLIST_API_PATH = 'playlist'; -const TARGET_UPDATE_API_PATH = 'playlist/update'; -const WEBPLAYER_URL_API_PATH = 'playlist/webplayer'; - -export default interface PlaylistApiService extends ApiService { - getPlaylist(req: PlaylistRequest): Observable; - - updateTargets(targets: string[]): Observable; - - getWebPlayerUrl(item: PlaylistItem, req: PlaylistRequest): Observable; -} - -export class DefaultPlaylistApiService extends DefaultApiService implements PlaylistApiService { - getPlaylist(req: PlaylistRequest): Observable { - // eslint-disable-next-line eqeqeq - if (req != undefined) { - return this.post(PLAYLIST_API_PATH, req); - } - return throwError(() => new Error('Invalid arguments')); - } - - updateTargets(targets: string[]): Observable { - return this.post(TARGET_UPDATE_API_PATH, targets); - } - - getWebPlayerUrl(item: PlaylistItem, req: PlaylistRequest): Observable { - return this.post(WEBPLAYER_URL_API_PATH + '/' + req.sourceId, item); - } - -} diff --git a/frontend/src/api/server-status-api-service.ts b/frontend/src/api/server-status-api-service.ts deleted file mode 100644 index bd6f799a8..000000000 --- a/frontend/src/api/server-status-api-service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {Observable} from "rxjs"; -import ApiService, {DefaultApiService} from "./api-service"; -import {ServerIpCheck, ServerStatus} from "../model/server-status"; - -const STATUS_PATH = "status"; -const IPCHECK_PATH = "ipinfo"; - -export default interface ServerStatusApiService extends ApiService { - getServerStatus(): Observable; - - getServerIpCheck(): Observable; -} - -export class DefaultServerStatusApiService extends DefaultApiService implements ServerStatusApiService { - - getServerStatus(): Observable { - return this.get(STATUS_PATH); - } - - getServerIpCheck(): Observable { - return this.get(IPCHECK_PATH); - } -} diff --git a/frontend/src/api/user-config-api-service.ts b/frontend/src/api/user-config-api-service.ts deleted file mode 100644 index 1cd8cf826..000000000 --- a/frontend/src/api/user-config-api-service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {Observable} from "rxjs"; -import ApiService, {DefaultApiService} from "./api-service"; -import {UserPlaylistCategories} from "../model/playlist"; - -const PLAYLIST_CATEGORIES_PATH = "user/playlist/categories"; -const PLAYLIST_BOUQUET_PATH = "user/playlist/bouquet"; - -export default interface UserConfigApiService extends ApiService { - getPlaylistBouquet(): Observable; - getPlaylistCategories(): Observable; - savePlaylistBouquet(bouquet: UserPlaylistCategories): Observable; -} - -export class DefaultUserConfigApiService extends DefaultApiService implements UserConfigApiService { - - getPlaylistBouquet(): Observable { - return this.get(PLAYLIST_BOUQUET_PATH); - } - getPlaylistCategories(): Observable { - return this.get(PLAYLIST_CATEGORIES_PATH); - } - - savePlaylistBouquet(bouquet: UserPlaylistCategories): Observable { - return this.post(PLAYLIST_BOUQUET_PATH, bouquet); - } -} diff --git a/frontend/src/app-user/app-user.scss b/frontend/src/app-user/app-user.scss deleted file mode 100755 index e85a82e65..000000000 --- a/frontend/src/app-user/app-user.scss +++ /dev/null @@ -1,73 +0,0 @@ -@use '../scss/common'; - -.user-app { - display: flex; - flex-flow: column; - justify-content: center; - overflow: hidden; - flex: 1 1 auto; - box-sizing: border-box; - max-width: 100%; - color: var(--text-color); - - &__header { - display: flex; - flex-flow: row; - font-size: 2rem; - font-weight: bold; - color: var(--app-header-color); - align-content: center; - padding: 10px; - gap: 8px; - @media (max-width: common.$media-size-mobile) { - padding: 10px 0; - } - - &__logo { - display: flex; - justify-content: center; - align-items: center; - svg, img { - width: 2em; - aspect-ratio: 1 / 1; - transform: translate(-10px, 0); - } - } - - &__caption { - display: flex; - flex-flow: row nowrap; - flex: 1 1 auto; - justify-content: center; - align-items: center; - } - - &__toolbar { - display: flex; - align-items: center; - flex: 0 0 auto; - button { - @include common.iconButton(); - } - padding-right: 4px; - } - } - - &__main { - display: flex; - flex-flow: column; - justify-content: center; - flex: 1 1 auto; - overflow: hidden; - box-sizing: border-box; - max-width: 100%; - } - &__content { - display: flex; - flex-flow: column; - flex: 1 1 auto; - box-sizing: border-box; - overflow: hidden; - max-width: 100%; - } -} \ No newline at end of file diff --git a/frontend/src/app-user/app-user.tsx b/frontend/src/app-user/app-user.tsx deleted file mode 100755 index 6a9060a0b..000000000 --- a/frontend/src/app-user/app-user.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, {useCallback, useMemo} from 'react'; -import './app-user.scss'; -import {getIconByName} from "../icons/icons"; -import {useServices} from "../provider/service-provider"; -import UserPlaylist from "../component/user-playlist/user-playlist"; -import {LANGUAGE} from "../hook/use-translator"; -import i18next from "i18next"; - -/* eslint-disable @typescript-eslint/no-empty-interface */ -interface AppUserProps { - -} - -export default function AppUser(props: AppUserProps) { - const services = useServices(); - const appTitle = useMemo(() => services.config().getUiConfig().app_title ?? 'tuliprox', [services]); - const appLogo = useMemo(() => { - let logo = services.config().getUiConfig().app_logo; - if (logo) { - return logo; - } else { - return getIconByName('Logo'); - } - }, [services]); - const handleLogout = useCallback(() => { - services.auth().logout(); - }, [services]); - - const handleLanguage = useCallback((event: any) => { - const language = event.target.value; - LANGUAGE.next(language); - }, []); - - - return ( -
-
-
{appLogo}{appTitle}
-
-
-
-
-
- -
-
-
- ); -} diff --git a/frontend/src/app/app.scss b/frontend/src/app/app.scss deleted file mode 100755 index 9035d7e89..000000000 --- a/frontend/src/app/app.scss +++ /dev/null @@ -1,76 +0,0 @@ -@use '../scss/common'; - -.app { - display: flex; - flex-flow: column; - justify-content: center; - overflow: hidden; - flex: 1 1 auto; - box-sizing: border-box; - max-width: 100%; - color: var(--text-color); - background-color: inherit; - - .app-header { - display: flex; - flex-flow: row; - font-size: 2rem; - font-weight: bold; - color: var(--app-header-color); - align-content: center; - padding: 10px; - @media (max-width: common.$media-size-mobile) { - padding: 10px 0; - } - - &__logo { - display: flex; - justify-content: center; - align-items: center; - - svg, img { - width: 2em; - aspect-ratio: 1 / 1; - transform: translate(-10px, 0); - } - } - - &__caption { - display: flex; - flex-flow: row nowrap; - flex: 1 1 auto; - justify-content: center; - align-items: center; - } - - &__toolbar { - display: flex; - align-items: center; - button { - @include common.iconButton(); - } - padding-right: 4px; - } - } - - .app-main { - display: flex; - flex-flow: column; - justify-content: center; - flex: 1 1 auto; - overflow: hidden; - box-sizing: border-box; - max-width: 100%; - background-color: inherit; - - .app-content { - display: flex; - flex-flow: column; - flex: 1 1 auto; - box-sizing: border-box; - overflow: hidden; - max-width: 100%; - background-color: inherit; - } - } -} \ No newline at end of file diff --git a/frontend/src/app/app.tsx b/frontend/src/app/app.tsx deleted file mode 100755 index 5bc491897..000000000 --- a/frontend/src/app/app.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import './app.scss'; -import {useSnackbar} from 'notistack'; -import {useServices} from "../provider/service-provider"; -import {first} from "rxjs/operators"; -import {noop} from "rxjs"; -import ServerConfig from "../model/server-config"; -import {getIconByName} from "../icons/icons"; -import Preferences from "../component/preferences/preferences"; -import useTranslator, {LANGUAGE} from "../hook/use-translator"; -import i18next from "i18next"; -import PlaylistBrowser from '../component/playlist-browser/playlist-browser'; -import SetupWizard from "../component/setup-wizard/setup-wizard"; - -/* eslint-disable @typescript-eslint/no-empty-interface */ -interface AppProps { - -} - -export default function App(props: AppProps) { - const services = useServices(); - const [serverConfig, setServerConfig] = useState(undefined); - const serverConfigLoaded = useRef(false); - const [preferencesVisible, setPreferencesVisible] = useState(true); - const appTitle = useMemo(() => services.config().getUiConfig().app_title ?? 'tuliprox', [services]); - const translate = useTranslator(); - const appLogo = useMemo(() => { - let logo = services.config().getUiConfig().app_logo; - if (logo) { - return logo; - } else { - return getIconByName('Logo'); - } - }, [services]); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - - useEffect(() => { - if (!serverConfigLoaded.current) { - serverConfigLoaded.current = true; - services.config().getServerConfig().pipe(first()).subscribe({ - next: (cfg: ServerConfig) => { - setServerConfig(cfg); - }, - error: (err) => { - enqueueSnackbar(translate("MESSAGES.DOWNLOAD.SERVER_CONFIG.FAIL"), {variant: 'error'}); - }, - complete: noop, - }); - } - return noop - }, [enqueueSnackbar, services, translate]); - - const handlePreferences = useCallback(() => { - setPreferencesVisible((value:boolean) => !value); - }, []); - - const handleLogout = useCallback(() => { - services.auth().logout(); - }, [services]); - - const handleLanguage = useCallback((event: any) => { - const language = event.target.value; - LANGUAGE.next(language); - }, []); - - return ( -
-
-
{appLogo}{appTitle}
-
-
-
-
- {/*
*/} - {/*
*/} - {/* */} - {/*
*/} - {/*
*/} - -
-
- -
-
-
- -
-
- ); -} diff --git a/webui/src/app/components/authentication.rs b/frontend/src/app/components/authentication.rs similarity index 100% rename from webui/src/app/components/authentication.rs rename to frontend/src/app/components/authentication.rs diff --git a/webui/src/app/components/breadcrumbs.rs b/frontend/src/app/components/breadcrumbs.rs similarity index 100% rename from webui/src/app/components/breadcrumbs.rs rename to frontend/src/app/components/breadcrumbs.rs diff --git a/webui/src/app/components/card.rs b/frontend/src/app/components/card.rs similarity index 100% rename from webui/src/app/components/card.rs rename to frontend/src/app/components/card.rs diff --git a/webui/src/app/components/chip.rs b/frontend/src/app/components/chip.rs similarity index 100% rename from webui/src/app/components/chip.rs rename to frontend/src/app/components/chip.rs diff --git a/webui/src/app/components/collapse_panel.rs b/frontend/src/app/components/collapse_panel.rs similarity index 100% rename from webui/src/app/components/collapse_panel.rs rename to frontend/src/app/components/collapse_panel.rs diff --git a/webui/src/app/components/confirm_dialog.rs b/frontend/src/app/components/confirm_dialog.rs similarity index 100% rename from webui/src/app/components/confirm_dialog.rs rename to frontend/src/app/components/confirm_dialog.rs diff --git a/webui/src/app/components/content_dialog.rs b/frontend/src/app/components/content_dialog.rs similarity index 100% rename from webui/src/app/components/content_dialog.rs rename to frontend/src/app/components/content_dialog.rs diff --git a/webui/src/app/components/custom_dialog.rs b/frontend/src/app/components/custom_dialog.rs similarity index 100% rename from webui/src/app/components/custom_dialog.rs rename to frontend/src/app/components/custom_dialog.rs diff --git a/webui/src/app/components/dashboard/action_card.rs b/frontend/src/app/components/dashboard/action_card.rs similarity index 100% rename from webui/src/app/components/dashboard/action_card.rs rename to frontend/src/app/components/dashboard/action_card.rs diff --git a/webui/src/app/components/dashboard/dashboard_view.rs b/frontend/src/app/components/dashboard/dashboard_view.rs similarity index 100% rename from webui/src/app/components/dashboard/dashboard_view.rs rename to frontend/src/app/components/dashboard/dashboard_view.rs diff --git a/webui/src/app/components/dashboard/discord_action_card.rs b/frontend/src/app/components/dashboard/discord_action_card.rs similarity index 100% rename from webui/src/app/components/dashboard/discord_action_card.rs rename to frontend/src/app/components/dashboard/discord_action_card.rs diff --git a/webui/src/app/components/dashboard/documentation_action_card.rs b/frontend/src/app/components/dashboard/documentation_action_card.rs similarity index 100% rename from webui/src/app/components/dashboard/documentation_action_card.rs rename to frontend/src/app/components/dashboard/documentation_action_card.rs diff --git a/webui/src/app/components/dashboard/github_action_card.rs b/frontend/src/app/components/dashboard/github_action_card.rs similarity index 100% rename from webui/src/app/components/dashboard/github_action_card.rs rename to frontend/src/app/components/dashboard/github_action_card.rs diff --git a/webui/src/app/components/dashboard/ipinfo_action_card.rs b/frontend/src/app/components/dashboard/ipinfo_action_card.rs similarity index 100% rename from webui/src/app/components/dashboard/ipinfo_action_card.rs rename to frontend/src/app/components/dashboard/ipinfo_action_card.rs diff --git a/webui/src/app/components/dashboard/mod.rs b/frontend/src/app/components/dashboard/mod.rs similarity index 100% rename from webui/src/app/components/dashboard/mod.rs rename to frontend/src/app/components/dashboard/mod.rs diff --git a/webui/src/app/components/dashboard/playlist_progress_status_card.rs b/frontend/src/app/components/dashboard/playlist_progress_status_card.rs similarity index 100% rename from webui/src/app/components/dashboard/playlist_progress_status_card.rs rename to frontend/src/app/components/dashboard/playlist_progress_status_card.rs diff --git a/webui/src/app/components/dashboard/stats_view.rs b/frontend/src/app/components/dashboard/stats_view.rs similarity index 100% rename from webui/src/app/components/dashboard/stats_view.rs rename to frontend/src/app/components/dashboard/stats_view.rs diff --git a/webui/src/app/components/dashboard/status_card.rs b/frontend/src/app/components/dashboard/status_card.rs similarity index 100% rename from webui/src/app/components/dashboard/status_card.rs rename to frontend/src/app/components/dashboard/status_card.rs diff --git a/webui/src/app/components/dashboard/user_action_card.rs b/frontend/src/app/components/dashboard/user_action_card.rs similarity index 100% rename from webui/src/app/components/dashboard/user_action_card.rs rename to frontend/src/app/components/dashboard/user_action_card.rs diff --git a/webui/src/app/components/dashboard/version_action_card.rs b/frontend/src/app/components/dashboard/version_action_card.rs similarity index 100% rename from webui/src/app/components/dashboard/version_action_card.rs rename to frontend/src/app/components/dashboard/version_action_card.rs diff --git a/webui/src/app/components/drop_down_icon_button.rs b/frontend/src/app/components/drop_down_icon_button.rs similarity index 100% rename from webui/src/app/components/drop_down_icon_button.rs rename to frontend/src/app/components/drop_down_icon_button.rs diff --git a/webui/src/app/components/hide_content.rs b/frontend/src/app/components/hide_content.rs similarity index 100% rename from webui/src/app/components/hide_content.rs rename to frontend/src/app/components/hide_content.rs diff --git a/webui/src/app/components/home.rs b/frontend/src/app/components/home.rs similarity index 100% rename from webui/src/app/components/home.rs rename to frontend/src/app/components/home.rs diff --git a/webui/src/app/components/icon_button.rs b/frontend/src/app/components/icon_button.rs similarity index 100% rename from webui/src/app/components/icon_button.rs rename to frontend/src/app/components/icon_button.rs diff --git a/webui/src/app/components/input.rs b/frontend/src/app/components/input.rs similarity index 100% rename from webui/src/app/components/input.rs rename to frontend/src/app/components/input.rs diff --git a/webui/src/app/components/loading_indicator.rs b/frontend/src/app/components/loading_indicator.rs similarity index 100% rename from webui/src/app/components/loading_indicator.rs rename to frontend/src/app/components/loading_indicator.rs diff --git a/webui/src/app/components/login.rs b/frontend/src/app/components/login.rs similarity index 100% rename from webui/src/app/components/login.rs rename to frontend/src/app/components/login.rs diff --git a/webui/src/app/components/menu_item.rs b/frontend/src/app/components/menu_item.rs similarity index 100% rename from webui/src/app/components/menu_item.rs rename to frontend/src/app/components/menu_item.rs diff --git a/webui/src/app/components/mod.rs b/frontend/src/app/components/mod.rs similarity index 100% rename from webui/src/app/components/mod.rs rename to frontend/src/app/components/mod.rs diff --git a/webui/src/app/components/no_content.rs b/frontend/src/app/components/no_content.rs similarity index 100% rename from webui/src/app/components/no_content.rs rename to frontend/src/app/components/no_content.rs diff --git a/webui/src/app/components/panel.rs b/frontend/src/app/components/panel.rs similarity index 100% rename from webui/src/app/components/panel.rs rename to frontend/src/app/components/panel.rs diff --git a/webui/src/app/components/playlist/assistant/mod.rs b/frontend/src/app/components/playlist/assistant/mod.rs similarity index 100% rename from webui/src/app/components/playlist/assistant/mod.rs rename to frontend/src/app/components/playlist/assistant/mod.rs diff --git a/webui/src/app/components/playlist/assistant/playlist.rs b/frontend/src/app/components/playlist/assistant/playlist.rs similarity index 100% rename from webui/src/app/components/playlist/assistant/playlist.rs rename to frontend/src/app/components/playlist/assistant/playlist.rs diff --git a/webui/src/app/components/playlist/assistant/playlist_assistant_context.rs b/frontend/src/app/components/playlist/assistant/playlist_assistant_context.rs similarity index 100% rename from webui/src/app/components/playlist/assistant/playlist_assistant_context.rs rename to frontend/src/app/components/playlist/assistant/playlist_assistant_context.rs diff --git a/webui/src/app/components/playlist/assistant/step_name.rs b/frontend/src/app/components/playlist/assistant/step_name.rs similarity index 100% rename from webui/src/app/components/playlist/assistant/step_name.rs rename to frontend/src/app/components/playlist/assistant/step_name.rs diff --git a/webui/src/app/components/playlist/assistant/step_schedule.rs b/frontend/src/app/components/playlist/assistant/step_schedule.rs similarity index 100% rename from webui/src/app/components/playlist/assistant/step_schedule.rs rename to frontend/src/app/components/playlist/assistant/step_schedule.rs diff --git a/webui/src/app/components/playlist/assistant/step_type.rs b/frontend/src/app/components/playlist/assistant/step_type.rs similarity index 100% rename from webui/src/app/components/playlist/assistant/step_type.rs rename to frontend/src/app/components/playlist/assistant/step_type.rs diff --git a/webui/src/app/components/playlist/create.rs b/frontend/src/app/components/playlist/create.rs similarity index 100% rename from webui/src/app/components/playlist/create.rs rename to frontend/src/app/components/playlist/create.rs diff --git a/webui/src/app/components/playlist/filter_view.rs b/frontend/src/app/components/playlist/filter_view.rs similarity index 100% rename from webui/src/app/components/playlist/filter_view.rs rename to frontend/src/app/components/playlist/filter_view.rs diff --git a/webui/src/app/components/playlist/input/input_headers.rs b/frontend/src/app/components/playlist/input/input_headers.rs similarity index 100% rename from webui/src/app/components/playlist/input/input_headers.rs rename to frontend/src/app/components/playlist/input/input_headers.rs diff --git a/webui/src/app/components/playlist/input/input_options.rs b/frontend/src/app/components/playlist/input/input_options.rs similarity index 100% rename from webui/src/app/components/playlist/input/input_options.rs rename to frontend/src/app/components/playlist/input/input_options.rs diff --git a/webui/src/app/components/playlist/input/input_type_view.rs b/frontend/src/app/components/playlist/input/input_type_view.rs similarity index 100% rename from webui/src/app/components/playlist/input/input_type_view.rs rename to frontend/src/app/components/playlist/input/input_type_view.rs diff --git a/webui/src/app/components/playlist/input/mod.rs b/frontend/src/app/components/playlist/input/mod.rs similarity index 100% rename from webui/src/app/components/playlist/input/mod.rs rename to frontend/src/app/components/playlist/input/mod.rs diff --git a/webui/src/app/components/playlist/input_table.rs b/frontend/src/app/components/playlist/input_table.rs similarity index 100% rename from webui/src/app/components/playlist/input_table.rs rename to frontend/src/app/components/playlist/input_table.rs diff --git a/webui/src/app/components/playlist/list.rs b/frontend/src/app/components/playlist/list.rs similarity index 100% rename from webui/src/app/components/playlist/list.rs rename to frontend/src/app/components/playlist/list.rs diff --git a/webui/src/app/components/playlist/mapper_script_view.rs b/frontend/src/app/components/playlist/mapper_script_view.rs similarity index 86% rename from webui/src/app/components/playlist/mapper_script_view.rs rename to frontend/src/app/components/playlist/mapper_script_view.rs index d8c7c07f5..2e221945a 100644 --- a/webui/src/app/components/playlist/mapper_script_view.rs +++ b/frontend/src/app/components/playlist/mapper_script_view.rs @@ -37,6 +37,17 @@ struct FormatParams { pub p_count: usize, } +impl FormatParams { + fn inc_level(&mut self, inc: usize) -> &mut Self { + self.level += inc; + self + } + fn dec_level(&mut self, dec: usize) -> &mut Self { + self.level -= dec; + self + } +} + // Indents with spaces for pretty printing fn indent(level: usize, do_indent: bool) -> Html { if do_indent { @@ -47,7 +58,7 @@ fn indent(level: usize, do_indent: bool) -> Html { } } -fn newline(format_params: &FormatParams) -> Html { +fn newline(format_params: &mut FormatParams) -> Html { if format_params.pretty { html! {
} } else { @@ -55,7 +66,7 @@ fn newline(format_params: &FormatParams) -> Html { } } -fn render_args(args: &[ExprId], script: &MapperScript, format_params: &FormatParams) -> Html { +fn render_args(args: &[ExprId], script: &MapperScript, format_params: &mut FormatParams) -> Html { html! { <> { @@ -87,7 +98,7 @@ fn render_map_key(key: &MapKey) -> Html { } } -fn render_function_call(name: &BuiltInFunction, args: &[ExprId], script: &MapperScript, format_params: &FormatParams) -> Html { +fn render_function_call(name: &BuiltInFunction, args: &[ExprId], script: &MapperScript, format_params: &mut FormatParams) -> Html { html! { {name.to_string()}{"("}{render_args(args, script, format_params)}{")"} } @@ -105,7 +116,7 @@ fn render_null_value() -> Html { html! { { "null" } } } -fn render_map_case(case: &MapCase, script: &MapperScript, format_params: &FormatParams) -> Html { +fn render_map_case(case: &MapCase, script: &MapperScript, format_params: &mut FormatParams) -> Html { let keys_html = html! { <> { @@ -131,6 +142,7 @@ fn render_map_case(case: &MapCase, script: &MapperScript, format_params: &Format let has_bracket = case.keys.len() > 1; html! { <> + {indent(format_params.level, true)} {if has_bracket {"("} else {""}} {keys_html} {if has_bracket {")"} else {""}} @@ -141,7 +153,7 @@ fn render_map_case(case: &MapCase, script: &MapperScript, format_params: &Format } -fn render_map_cases(cases: &[MapCase], script: &MapperScript, format_params: &FormatParams) -> Html { +fn render_map_cases(cases: &[MapCase], script: &MapperScript, format_params: &mut FormatParams) -> Html { html!{ <> { @@ -151,22 +163,24 @@ fn render_map_cases(cases: &[MapCase], script: &MapperScript, format_params: &Fo } } -fn render_map_block(map_key: &MapKey, cases: &[MapCase], script: &MapperScript, format_params: &FormatParams) -> Html { +fn render_map_block(map_key: &MapKey, cases: &[MapCase], script: &MapperScript, format_params: &mut FormatParams) -> Html { html! { - <> {"map "} + <> + {indent(format_params.level, true)} + {"map "} {render_map_key(map_key)} {" {"} {newline(format_params)} - {render_map_cases(cases, script, format_params)} - {newline(format_params)} + {render_map_cases(cases, script, format_params.inc_level(1))} + {newline(format_params.dec_level(1))} + {indent(format_params.level, true)} {"}"} - {newline(format_params)} } } -fn render_block(expr_ids: &[ExprId], script: &MapperScript, format_params: &FormatParams) -> Html { +fn render_block(expr_ids: &[ExprId], script: &MapperScript, format_params: &mut FormatParams) -> Html { html! { <> {"{"} @@ -188,7 +202,7 @@ fn render_block(expr_ids: &[ExprId], script: &MapperScript, format_params: &Form } } -fn render_assignment(target: &AssignmentTarget, expr_id: &ExprId, script: &MapperScript, format_params: &FormatParams) -> Html { +fn render_assignment(target: &AssignmentTarget, expr_id: &ExprId, script: &MapperScript, format_params: &mut FormatParams) -> Html { let target_html = match target { AssignmentTarget::Identifier(ident) => render_identifier(ident), AssignmentTarget::Field(field) => render_field(field), @@ -203,7 +217,7 @@ fn render_assignment(target: &AssignmentTarget, expr_id: &ExprId, script: &Mappe } } -fn render_match_block(match_cases: &[MatchCase], script: &MapperScript, format_params: &FormatParams) -> Html { +fn render_match_block(match_cases: &[MatchCase], script: &MapperScript, format_params: &mut FormatParams) -> Html { html! { <> {"!! TODO MATCH BLOCK !!!"} @@ -224,7 +238,7 @@ fn render_regexp(field: &RegexSource, pattern: &String, regex: &Regex) -> Html { html! { <> {render_regex_source(field)} {"~ '"}{ pattern } {"'"} } } -fn render_expression(expr_id: &ExprId, script: &MapperScript, format_params: &FormatParams) -> Html { +fn render_expression(expr_id: &ExprId, script: &MapperScript, format_params: &mut FormatParams) -> Html { script.get_expr_by_id(*expr_id.deref()).map(|expression| { match expression { Expression::Identifier(ident) => render_identifier(ident), @@ -244,15 +258,15 @@ fn render_expression(expr_id: &ExprId, script: &MapperScript, format_params: &Fo } fn render_script(script: &MapperScript, pretty: bool, level: usize, do_indent: bool, p_count: usize) -> Html { - let format_params = FormatParams { + let mut format_params = FormatParams { pretty, level, do_indent, p_count }; let items = script.statements.iter().map(|stmt| { match stmt { Statement::Expression(expr_id) => html! { <> - {render_expression(expr_id, script, &format_params)} - {newline(&format_params)} + {render_expression(expr_id, script, &mut format_params)} + {newline(&mut format_params)} }, Statement::Comment(comment) => html!{
{ comment }
}, diff --git a/webui/src/app/components/playlist/mappings.rs b/frontend/src/app/components/playlist/mappings.rs similarity index 92% rename from webui/src/app/components/playlist/mappings.rs rename to frontend/src/app/components/playlist/mappings.rs index 6c1d00628..7e134e104 100644 --- a/webui/src/app/components/playlist/mappings.rs +++ b/frontend/src/app/components/playlist/mappings.rs @@ -31,8 +31,7 @@ pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html { let render_mapper = |mapper: &MapperDto| { html! { -
- +
@@ -59,14 +58,14 @@ pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html {
-
+
{ for mapping.mapper.iter().flatten().map(|mapper| { html! { render_mapper(mapper) } }) }
-
+
{ for mapping.counter.iter().flatten().map(|counter| { html! { render_counter(counter) } diff --git a/webui/src/app/components/playlist/mod.rs b/frontend/src/app/components/playlist/mod.rs similarity index 100% rename from webui/src/app/components/playlist/mod.rs rename to frontend/src/app/components/playlist/mod.rs diff --git a/webui/src/app/components/playlist/playlist_editor_page.rs b/frontend/src/app/components/playlist/playlist_editor_page.rs similarity index 100% rename from webui/src/app/components/playlist/playlist_editor_page.rs rename to frontend/src/app/components/playlist/playlist_editor_page.rs diff --git a/webui/src/app/components/playlist/playlist_editor_view.rs b/frontend/src/app/components/playlist/playlist_editor_view.rs similarity index 100% rename from webui/src/app/components/playlist/playlist_editor_view.rs rename to frontend/src/app/components/playlist/playlist_editor_view.rs diff --git a/webui/src/app/components/playlist/playlist_explorer.rs b/frontend/src/app/components/playlist/playlist_explorer.rs similarity index 100% rename from webui/src/app/components/playlist/playlist_explorer.rs rename to frontend/src/app/components/playlist/playlist_explorer.rs diff --git a/webui/src/app/components/playlist/playlist_explorer_page.rs b/frontend/src/app/components/playlist/playlist_explorer_page.rs similarity index 100% rename from webui/src/app/components/playlist/playlist_explorer_page.rs rename to frontend/src/app/components/playlist/playlist_explorer_page.rs diff --git a/webui/src/app/components/playlist/playlist_explorer_view.rs b/frontend/src/app/components/playlist/playlist_explorer_view.rs similarity index 100% rename from webui/src/app/components/playlist/playlist_explorer_view.rs rename to frontend/src/app/components/playlist/playlist_explorer_view.rs diff --git a/webui/src/app/components/playlist/playlist_update_view.rs b/frontend/src/app/components/playlist/playlist_update_view.rs similarity index 100% rename from webui/src/app/components/playlist/playlist_update_view.rs rename to frontend/src/app/components/playlist/playlist_update_view.rs diff --git a/webui/src/app/components/playlist/processing.rs b/frontend/src/app/components/playlist/processing.rs similarity index 100% rename from webui/src/app/components/playlist/processing.rs rename to frontend/src/app/components/playlist/processing.rs diff --git a/webui/src/app/components/playlist/source_selector.rs b/frontend/src/app/components/playlist/source_selector.rs similarity index 100% rename from webui/src/app/components/playlist/source_selector.rs rename to frontend/src/app/components/playlist/source_selector.rs diff --git a/webui/src/app/components/playlist/target/hdhomerun_output.rs b/frontend/src/app/components/playlist/target/hdhomerun_output.rs similarity index 100% rename from webui/src/app/components/playlist/target/hdhomerun_output.rs rename to frontend/src/app/components/playlist/target/hdhomerun_output.rs diff --git a/webui/src/app/components/playlist/target/m3u_output.rs b/frontend/src/app/components/playlist/target/m3u_output.rs similarity index 100% rename from webui/src/app/components/playlist/target/m3u_output.rs rename to frontend/src/app/components/playlist/target/m3u_output.rs diff --git a/webui/src/app/components/playlist/target/mod.rs b/frontend/src/app/components/playlist/target/mod.rs similarity index 100% rename from webui/src/app/components/playlist/target/mod.rs rename to frontend/src/app/components/playlist/target/mod.rs diff --git a/webui/src/app/components/playlist/target/strm_output.rs b/frontend/src/app/components/playlist/target/strm_output.rs similarity index 100% rename from webui/src/app/components/playlist/target/strm_output.rs rename to frontend/src/app/components/playlist/target/strm_output.rs diff --git a/webui/src/app/components/playlist/target/target_options.rs b/frontend/src/app/components/playlist/target/target_options.rs similarity index 100% rename from webui/src/app/components/playlist/target/target_options.rs rename to frontend/src/app/components/playlist/target/target_options.rs diff --git a/webui/src/app/components/playlist/target/target_output.rs b/frontend/src/app/components/playlist/target/target_output.rs similarity index 100% rename from webui/src/app/components/playlist/target/target_output.rs rename to frontend/src/app/components/playlist/target/target_output.rs diff --git a/webui/src/app/components/playlist/target/target_rename.rs b/frontend/src/app/components/playlist/target/target_rename.rs similarity index 100% rename from webui/src/app/components/playlist/target/target_rename.rs rename to frontend/src/app/components/playlist/target/target_rename.rs diff --git a/webui/src/app/components/playlist/target/target_sort.rs b/frontend/src/app/components/playlist/target/target_sort.rs similarity index 100% rename from webui/src/app/components/playlist/target/target_sort.rs rename to frontend/src/app/components/playlist/target/target_sort.rs diff --git a/webui/src/app/components/playlist/target/target_watch.rs b/frontend/src/app/components/playlist/target/target_watch.rs similarity index 100% rename from webui/src/app/components/playlist/target/target_watch.rs rename to frontend/src/app/components/playlist/target/target_watch.rs diff --git a/webui/src/app/components/playlist/target/xtream_output.rs b/frontend/src/app/components/playlist/target/xtream_output.rs similarity index 100% rename from webui/src/app/components/playlist/target/xtream_output.rs rename to frontend/src/app/components/playlist/target/xtream_output.rs diff --git a/webui/src/app/components/playlist/target_table.rs b/frontend/src/app/components/playlist/target_table.rs similarity index 100% rename from webui/src/app/components/playlist/target_table.rs rename to frontend/src/app/components/playlist/target_table.rs diff --git a/webui/src/app/components/popup_menu.rs b/frontend/src/app/components/popup_menu.rs similarity index 100% rename from webui/src/app/components/popup_menu.rs rename to frontend/src/app/components/popup_menu.rs diff --git a/webui/src/app/components/radio_button_group.rs b/frontend/src/app/components/radio_button_group.rs similarity index 100% rename from webui/src/app/components/radio_button_group.rs rename to frontend/src/app/components/radio_button_group.rs diff --git a/webui/src/app/components/reveal_content.rs b/frontend/src/app/components/reveal_content.rs similarity index 100% rename from webui/src/app/components/reveal_content.rs rename to frontend/src/app/components/reveal_content.rs diff --git a/webui/src/app/components/search.rs b/frontend/src/app/components/search.rs similarity index 100% rename from webui/src/app/components/search.rs rename to frontend/src/app/components/search.rs diff --git a/webui/src/app/components/sidebar.rs b/frontend/src/app/components/sidebar.rs similarity index 100% rename from webui/src/app/components/sidebar.rs rename to frontend/src/app/components/sidebar.rs diff --git a/webui/src/app/components/svg_icon.rs b/frontend/src/app/components/svg_icon.rs similarity index 100% rename from webui/src/app/components/svg_icon.rs rename to frontend/src/app/components/svg_icon.rs diff --git a/webui/src/app/components/table.rs b/frontend/src/app/components/table.rs similarity index 100% rename from webui/src/app/components/table.rs rename to frontend/src/app/components/table.rs diff --git a/webui/src/app/components/tag_list.rs b/frontend/src/app/components/tag_list.rs similarity index 100% rename from webui/src/app/components/tag_list.rs rename to frontend/src/app/components/tag_list.rs diff --git a/webui/src/app/components/text_button.rs b/frontend/src/app/components/text_button.rs similarity index 100% rename from webui/src/app/components/text_button.rs rename to frontend/src/app/components/text_button.rs diff --git a/webui/src/app/components/toastr.rs b/frontend/src/app/components/toastr.rs similarity index 100% rename from webui/src/app/components/toastr.rs rename to frontend/src/app/components/toastr.rs diff --git a/webui/src/app/components/toggle_switch.rs b/frontend/src/app/components/toggle_switch.rs similarity index 100% rename from webui/src/app/components/toggle_switch.rs rename to frontend/src/app/components/toggle_switch.rs diff --git a/webui/src/app/components/userlist/edit.rs b/frontend/src/app/components/userlist/edit.rs similarity index 100% rename from webui/src/app/components/userlist/edit.rs rename to frontend/src/app/components/userlist/edit.rs diff --git a/webui/src/app/components/userlist/list.rs b/frontend/src/app/components/userlist/list.rs similarity index 100% rename from webui/src/app/components/userlist/list.rs rename to frontend/src/app/components/userlist/list.rs diff --git a/webui/src/app/components/userlist/max_connections.rs b/frontend/src/app/components/userlist/max_connections.rs similarity index 100% rename from webui/src/app/components/userlist/max_connections.rs rename to frontend/src/app/components/userlist/max_connections.rs diff --git a/webui/src/app/components/userlist/mod.rs b/frontend/src/app/components/userlist/mod.rs similarity index 100% rename from webui/src/app/components/userlist/mod.rs rename to frontend/src/app/components/userlist/mod.rs diff --git a/webui/src/app/components/userlist/page.rs b/frontend/src/app/components/userlist/page.rs similarity index 100% rename from webui/src/app/components/userlist/page.rs rename to frontend/src/app/components/userlist/page.rs diff --git a/webui/src/app/components/userlist/proxy_type_view.rs b/frontend/src/app/components/userlist/proxy_type_view.rs similarity index 100% rename from webui/src/app/components/userlist/proxy_type_view.rs rename to frontend/src/app/components/userlist/proxy_type_view.rs diff --git a/webui/src/app/components/userlist/user_status.rs b/frontend/src/app/components/userlist/user_status.rs similarity index 100% rename from webui/src/app/components/userlist/user_status.rs rename to frontend/src/app/components/userlist/user_status.rs diff --git a/webui/src/app/components/userlist/user_table.rs b/frontend/src/app/components/userlist/user_table.rs similarity index 100% rename from webui/src/app/components/userlist/user_table.rs rename to frontend/src/app/components/userlist/user_table.rs diff --git a/webui/src/app/components/userlist/userlist_view.rs b/frontend/src/app/components/userlist/userlist_view.rs similarity index 100% rename from webui/src/app/components/userlist/userlist_view.rs rename to frontend/src/app/components/userlist/userlist_view.rs diff --git a/webui/src/app/context.rs b/frontend/src/app/context.rs similarity index 100% rename from webui/src/app/context.rs rename to frontend/src/app/context.rs diff --git a/webui/src/app/mod.rs b/frontend/src/app/mod.rs similarity index 100% rename from webui/src/app/mod.rs rename to frontend/src/app/mod.rs diff --git a/frontend/src/component/api-proxy-view/api-proxy-view.scss b/frontend/src/component/api-proxy-view/api-proxy-view.scss deleted file mode 100644 index e7d8f9255..000000000 --- a/frontend/src/component/api-proxy-view/api-proxy-view.scss +++ /dev/null @@ -1,36 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - - -.api-proxy { - - @include preferences.prefsRoot(); - @include preferences.prefsToolbar(); - @include preferences.prefsForm(); - - &__content { - display: flex; - flex-flow: column; - gap: 8px; - overflow: auto; - &-area { - display: flex; - flex-flow: row wrap; - gap: 12px; - padding: 12px; - } - } - - &__content-help { - display: flex; - flex-flow: row; - justify-content: center; - gap: 4px; - align-items: center; - &-warn-icon { - color: var(--warn-color); - } - } - -} - diff --git a/frontend/src/component/api-proxy-view/api-proxy-view.tsx b/frontend/src/component/api-proxy-view/api-proxy-view.tsx deleted file mode 100644 index 8eb9cf0fe..000000000 --- a/frontend/src/component/api-proxy-view/api-proxy-view.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, {useCallback, useMemo} from "react"; -import './api-proxy-view.scss'; -import ServerConfig, {ApiProxyServerInfo} from "../../model/server-config"; -import {useSnackbar} from "notistack"; -import {useServices} from "../../provider/service-provider"; -import FormView, {FormFieldType} from "../form-view/from-view"; -import {getIconByName} from "../../icons/icons"; -import useTranslator from "../../hook/use-translator"; - -const isNumber = (value: string): boolean => { - return !isNaN(value as any); -} - -const SERVER_INFO_FIELDS = [ - {name: 'name', label: 'LABEL.NAME', fieldType: FormFieldType.READONLY}, - {name: 'protocol', label: 'LABEL.PROTOCOL', fieldType: FormFieldType.SINGLE_SELECT, - options:[{value: 'http', label:'http'}, {value: 'https', label:'https'}]}, - {name: 'host', label: 'LABEL.HOST', fieldType: FormFieldType.TEXT}, - {name: 'port', label: 'LABEL.PORT', fieldType: FormFieldType.NUMBER, validator: isNumber}, - {name: 'timezone', label: 'LABEL.TIMEZONE', fieldType: FormFieldType.TEXT}, - {name: 'message', label: 'LABEL.MESSAGE', fieldType: FormFieldType.TEXT}, - {name: 'path', label: 'LABEL.PATH', hint: 'HINT.CONFIG.API_PROXY.PATH', fieldType: FormFieldType.TEXT}, -]; - -interface ApiProxyViewProps { - config: ServerConfig; -} - -export default function ApiProxyView(props: ApiProxyViewProps) { - const {config} = props; - const services = useServices(); - const translate = useTranslator(); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const serverInfo = useMemo(() => config?.api_proxy?.server, [config]); - - const handleSave = useCallback(() => { - if (serverInfo) { - // @TODO validations - services.config().saveApiProxyConfig(serverInfo).subscribe({ - next: () => enqueueSnackbar("Api proxy config saved!", {variant: 'success'}), - error: (err) => enqueueSnackbar("Failed to save api proxy config!", {variant: 'error'}) - }); - } - }, [services, serverInfo, enqueueSnackbar]); - - return
-
- -
-
-
- {serverInfo?.map((server, idx) =>
- -
) - } -
-
-
- {getIconByName('Warn')} - {translate('INFO.RESTART_TO_APPLY_CHANGES')} -
-
-} \ No newline at end of file diff --git a/frontend/src/component/authentication/authentication.tsx b/frontend/src/component/authentication/authentication.tsx deleted file mode 100644 index 65b32096c..000000000 --- a/frontend/src/component/authentication/authentication.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, {JSX, useEffect, useState} from 'react'; -import App from "../../app/app"; -import Login from "../login/login"; -import {useServices} from "../../provider/service-provider"; -import {first} from "rxjs/operators"; -import {noop, tap} from "rxjs"; -import {UserRole} from "../../service/auth-service"; -import AppUser from "../../app-user/app-user"; - -export default function Authentication(): JSX.Element { - - const services = useServices(); - const [loading, setLoading] = useState(true); - const [authenticated, setAuthenticated] = useState(UserRole.NONE); - - useEffect(() => { - const sub = services.auth().authChannel().subscribe({ - next: (auth) => { - setLoading(false); - setAuthenticated(auth); - }, - error: () => setAuthenticated(UserRole.NONE), - }) - - const noAuthCheck = () => services.auth().authenticate('test', 'test') - .pipe(tap(() => setLoading(false)), first()).subscribe(noop); - - services.auth().refresh().pipe(first()).subscribe({ - next: (authenticated) =>!authenticated && noAuthCheck(), - error: () => noAuthCheck(), - }); - - return () => sub.unsubscribe(); - }, [services]); - - if (loading) { - return <> - } - - if (authenticated === UserRole.ADMIN) { - return ; - } - - if (authenticated === UserRole.USER) { - return ; - } - - return -} \ No newline at end of file diff --git a/frontend/src/component/checkbox/checkbox.scss b/frontend/src/component/checkbox/checkbox.scss deleted file mode 100644 index 99c71565e..000000000 --- a/frontend/src/component/checkbox/checkbox.scss +++ /dev/null @@ -1,60 +0,0 @@ -$checkbox-size:18px; -.checkbox-container { - display: block; - position: relative; - padding-left: 24px; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - - input { - position: relative; - opacity: 0; - cursor: pointer; - height: 0; - width: 0; - } - - .checkmark { - position: absolute; - top: 0; - left: 0; - height: $checkbox-size; - width: $checkbox-size; - background-color: var(--checkbox-background-color); - border-radius: var(--checkbox-border-radius); - - &:after { - content: ""; - position: absolute; - display: none; - } - } - - &:hover input ~ .checkmark { - background-color: var(--checkbox-hover-background-color); - } - - input:checked ~ .checkmark { - background-color: var(--checkbox-checked-background-color); - } - - input:checked ~ .checkmark:after { - display: block; - } - - .checkmark:after { - left: 5px; - top: 1px; - width: 5px; - height: 10px; - border: solid white; - border-width: 0 3px 3px 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); - } - -} diff --git a/frontend/src/component/checkbox/checkbox.tsx b/frontend/src/component/checkbox/checkbox.tsx deleted file mode 100644 index 43c7e541d..000000000 --- a/frontend/src/component/checkbox/checkbox.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, {useCallback, useLayoutEffect, useRef} from "react"; -import './checkbox.scss'; - -interface CheckboxProps { - label?: string; - value: any; - checked: boolean; - onSelect: (value: any, checked: boolean, evt?: any) => void; -} - -export default function Checkbox(props: CheckboxProps) { - - const {label, value, onSelect, checked} = props; - const inputRef = useRef(undefined); - - const handleSelect = useCallback((evt: any) => { - evt.preventDefault(); - evt.stopPropagation(); - inputRef.current.checked = !inputRef.current.checked; - onSelect?.(value, inputRef.current.checked, evt); - }, [value, onSelect]); - - useLayoutEffect(() => { - if (inputRef.current) { - inputRef.current.checked = checked; - } - }, [checked]); - - if (label) { - return ; - } else { - return
- - -
- } -} \ No newline at end of file diff --git a/frontend/src/component/clipboard-viewer/clipboard-viewer.scss b/frontend/src/component/clipboard-viewer/clipboard-viewer.scss deleted file mode 100644 index dfd3328ba..000000000 --- a/frontend/src/component/clipboard-viewer/clipboard-viewer.scss +++ /dev/null @@ -1,43 +0,0 @@ -@use '../../scss/theme'; -@use '../../scss/common'; - -$viewer-height: 200px; - -.clipboard-viewer { - display: flex; - flex: 1 1 auto; - flex-flow: column; - - .clipboard-viewer-toolbar { - display: flex; - justify-content: flex-end; - flex-flow: row nowrap; - color: var(--text-color); - background-color: var(--toolbar-background-color); - padding: 8px 0 8px 0; - - .toolbar-btn { - cursor: pointer; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - background-color: transparent; - color: var(--toolbar-button-hover-color); - margin-right: 8px; - &:hover { - background-color: var(--toolbar-button-hover-background-color); - } - } - } - - .clipboard-viewer-content { - flex-grow: 1; - max-height: $viewer-height; - color: var(--text-color); - overflow: auto; - white-space: nowrap; - padding: 8px; - - @include common.list(); - - } -} \ No newline at end of file diff --git a/frontend/src/component/clipboard-viewer/clipboard-viewer.tsx b/frontend/src/component/clipboard-viewer/clipboard-viewer.tsx deleted file mode 100644 index 3dd1164c2..000000000 --- a/frontend/src/component/clipboard-viewer/clipboard-viewer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, {JSX, useCallback, useEffect, useState} from 'react'; -import './clipboard-viewer.scss'; -import {noop, Observable} from "rxjs"; -import copyToClipboard from "../../utils/clipboard"; -import {first} from "rxjs/operators"; -import {useSnackbar} from "notistack"; -import {getIconByName} from "../../icons/icons"; - -interface ClipboardViewerProps { - channel: Observable; -} - -export default function ClipboardViewer(props: ClipboardViewerProps): JSX.Element { - - const {channel} = props; - const [data, setData] = useState([]); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - - useEffect(() => { - const sub = channel.subscribe({ - next: (value: string) => setData(d => [...d, value]), - }); - return () => sub.unsubscribe(); - }, [channel]); - - const handleClear = useCallback(() => { - setData([]); - }, []); - - const handleCopyToClipboard = useCallback(() => { - if (data?.length) { - copyToClipboard(data.join('\n')).pipe(first()).subscribe({ - next: value => enqueueSnackbar(value ? "Copied to clipboard" : "Copy to clipboard failed!", - {variant: value ? 'success' : 'error'}), - error: err => enqueueSnackbar("Copy to clipboard failed!", {variant: 'error'}), - complete: noop, - }); - } - }, [data, enqueueSnackbar]); - - return
-
- - -
-
-
    - {data.map((t, i) =>
  • {t}
  • )} -
-
-
; -} \ No newline at end of file diff --git a/frontend/src/component/date-picker/date-picker.scss b/frontend/src/component/date-picker/date-picker.scss deleted file mode 100644 index 14f21c40c..000000000 --- a/frontend/src/component/date-picker/date-picker.scss +++ /dev/null @@ -1,75 +0,0 @@ -@use '../../scss/common'; -@import "react-datepicker/dist/react-datepicker.css"; - -.date-picker-container { - display: flex; - flex-flow: row nowrap; - align-items: center; - gap: 4px; - - &__toolbar { - display: flex; - flex-flow: row nowrap; - align-items: center; - gap: 4px; - button { - @include common.iconButton(); - } - svg { - fill: var(--icon-button-color); - } - } -} - -.react-datepicker-wrapper { - box-sizing: border-box; - overflow: hidden; -} - -.react-datepicker__header { - h2 { - display: none; - } -} - -.react-datepicker__navigation--years-upcoming { - width: 0; - height: 0; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 12px solid var(--border-color); - transform: translateY(4px); -} -.react-datepicker__navigation--years-previous { - width: 0; - height: 0; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-top: 12px solid var(--border-color); - transform: translateY(-4px); -} - -.react-datepicker__input-container { - display: flex; - flex-flow: row-reverse nowrap; - padding: 0 4px; - align-items: center; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - user-select: none; - cursor: pointer; - box-sizing: border-box; - - input { - user-select: none; - pointer-events: none; - border: none !important; - caret: transparent; - &:focus { - background-color: transparent !important; - } - } - svg { - fill: var(--icon-button-color); - } -} \ No newline at end of file diff --git a/frontend/src/component/date-picker/date-picker.tsx b/frontend/src/component/date-picker/date-picker.tsx deleted file mode 100644 index 176ad5932..000000000 --- a/frontend/src/component/date-picker/date-picker.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, {useCallback, useEffect} from "react"; -import "./date-picker.scss"; -import ReactDatePicker from "react-datepicker"; -import {getIconByName} from "../../icons/icons"; - - -interface DatePickerProps { - name: string; - value: any; - onChange: (name: string, values: string[]) => void; -} - -export default function DatePicker(props: DatePickerProps) { - const {name, value, onChange} = props; - const [selected, setSelected] = React.useState(value); - const datePickerRef = React.useRef(null); - - useEffect(() => { - setSelected(value); - }, [value]); - - const handleDateChange = useCallback((date: any) => { - setSelected(date); - datePickerRef.current.setOpen(false); - onChange(name, date); - }, [name, onChange]); - - const handleClick = useCallback(() => { - if (!datePickerRef.current.isCalendarOpen()) { - datePickerRef.current.setOpen(true); - } - }, []); - - const handleInfiniteDate = useCallback(() => { - handleDateChange(new Date(0)); - }, [handleDateChange]); - - const handleNowDate = useCallback(() => { - const date = new Date(); - date.setFullYear(date.getFullYear() + 1); - handleDateChange(date); - }, [handleDateChange]); - - return ( -
-
- - -
-
- - -
-
- ); -} diff --git a/frontend/src/component/file-download/file-download.scss b/frontend/src/component/file-download/file-download.scss deleted file mode 100644 index 3de12c389..000000000 --- a/frontend/src/component/file-download/file-download.scss +++ /dev/null @@ -1,76 +0,0 @@ -@use '../../scss/common'; - -.file-download { - display: flex; - flex-flow: column; - flex: 1 0 auto; - margin: 4px 10px; - overflow: hidden; - max-height: 12rem; - - .download-info { - display: flex; - flex-flow: column; - flex: 1 1 auto; - background-color: var(--popup-background-color); - color: var(--popup-color); - filter: drop-shadow(2px 2px 3px var(--popup-shadow-color)); - border: 1px solid var(--popup-border-color); - overflow: hidden; - - &__toolbar { - flex: 0 0 auto; - padding: 4px; - background-color: var(--background-color); - button { - @include common.iconButton(); - } - } - - &__content { - overflow: auto; - } - - @include common.list(); - - ul li { - display: flex; - flex-flow: row; - gap: 4px; - align-items: flex-start; - } - - &__content { - display: flex; - flex-flow: row wrap; - gap: 4px; - } - - &__filesize { - white-space: nowrap; - } - - &__error { - margin-left: 8px; - color: var(--attention-color); - } - - &__remove_btn { - @include common.iconButton(); - max-height: 0.8rem; - padding: 0 4px; - svg { - transform: scale(0.7); - } - } - - &__status { - max-height: 0.8rem; - padding: 0 4px; - svg { - transform: scale(0.7); - } - } - } -} - diff --git a/frontend/src/component/file-download/file-download.tsx b/frontend/src/component/file-download/file-download.tsx deleted file mode 100644 index 94456cec1..000000000 --- a/frontend/src/component/file-download/file-download.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, {useCallback, useEffect, useRef, useState} from "react"; -import './file-download.scss'; -import {Subscription} from "rxjs"; -import {DownloadInfo, FileDownloadInfo} from "../../model/file-download"; -import {useServices} from "../../provider/service-provider"; -import {useSnackbar} from "notistack"; -import {getIconByName} from "../../icons/icons"; - -interface FileDownloadProps { -} - -export default function FileDownload(props: FileDownloadProps) { - const services = useServices(); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const [downloads, setDownloads] = useState>({}) - const downloading = useRef(false); - - const startPoll = useCallback(() => { - if (!downloading.current) { - downloading.current = true; - let subs: Subscription = services.file().getDownloadInfo().subscribe({ - next: (info: DownloadInfo) => { - if (info.completed === true) { - downloading.current = false; - } - const new_downloads = info.downloads || []; - if (info.active) { - new_downloads.push(info.active); - } - if (new_downloads.length) { - setDownloads(downloads => { - new_downloads.forEach(d => downloads[d.uuid] = d); - return {...downloads}; - }); - } - }, - error: (err) => { - enqueueSnackbar("Download file failed!", {variant: 'error'}); - downloading.current = false; - }, - complete: () => { - subs.unsubscribe(); - downloading.current = false; - } - }); - } - }, [enqueueSnackbar, services]); - - const handleDeleteClick = useCallback((event: any) => { - const key = event.target.dataset.uuid; - setDownloads(downloads => { - delete downloads[key]; - return {...downloads}; - }); - }, []); - - const handleClearAll = useCallback(() => { - setDownloads({}); - }, []); - - useEffect(() => { - const sub = services.file().subscribeDownloadNotification((info: FileDownloadInfo) => { - setDownloads((downloads: any) => { - if (!downloads[info.uuid]) { - info.ts_created = Date.now(); - info.ts_modified = info.ts_created; - downloads[info.uuid] = info; - return {...downloads}; - } - return downloads; - }); - startPoll(); - }); - startPoll(); - return () => sub.unsubscribe(); - }, [services, startPoll]) - - const renderDownloads = useCallback((): React.ReactNode => { - const info_list = Object.keys(downloads).map(key => downloads[key]); - if (info_list.length) { - info_list.sort((a, b) => { - const ats = a.finished ? a.ts_created : a.ts_modified; - const bts = b.finished ? b.ts_created : b.ts_modified; - return bts - ats; - }) - return
-
-
- -
-
-
    - {info_list.map(download => { - return
  • - {getIconByName('Delete')} - - {getIconByName(download.error ? 'Error' : (download.finished ? 'CheckMark' : 'Hourglass'))} - {download.filename}: - {download.filesize ? (download.filesize / 1_048_576).toFixed(2) : 0} MB - - - {download.error && {download.error}} -
  • ; - }) - } -
-
-
-
; - } - return <> - }, [downloads, handleDeleteClick, handleClearAll]); - - return renderDownloads(); -} \ No newline at end of file diff --git a/frontend/src/component/form-view/form-view.scss b/frontend/src/component/form-view/form-view.scss deleted file mode 100644 index 4e695e6d9..000000000 --- a/frontend/src/component/form-view/form-view.scss +++ /dev/null @@ -1,88 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - -.form-view { - @include common.container_vert_no_padding(); - - .label-hint { - cursor: pointer; - svg { - fill: var(--text-color); - height: 0.7em; - } - } - - &__map-editor { - @include common.container_horz_nowrap(); - flex: 1 1 auto; - overflow: hidden; - padding:0; - } - - &__table { - @include common.container_vert_no_padding(); - } - - &__row { - flex-flow: row nowrap; - @include common.container_common(flex-start, 0); - @media (max-width: common.$media-size-mobile) { - flex-flow: column; - &:nth-child(even) { - background-color: var(--card-background-color-darker); - border-radius: var(--border-radius); - } - } - } - - &__col { - display: flex; - flex-flow: row; - padding: 6px; - - &-label { - display: flex; - min-width: 10em; - width: 10em; - justify-content: flex-start; - align-items: center; - @media (max-width: common.$media-size-mobile) { - justify-content: flex-start; - } - } - - &-value { - display: flex; - flex: 1 1 auto; - align-items: center; - overflow: hidden; - gap: 4px; - - button { - border: 1px solid var(--text-button-border-color); - font-size: 1.2rem; - height: 100%; - border-radius: var(--text-button-border-radius); - background-color: var(--text-button-background-color); - color: var(--text-button-color); - } - } - - label { - font-weight: bold; - color: var(--label-color); - - &:after { - content: ':' - } - } - // - //input { - // @include preferences.prefsInput(); - // flex: 1 1 0; - // min-width: 0; - // width: 100%; - //} - } -} - diff --git a/frontend/src/component/form-view/from-view.tsx b/frontend/src/component/form-view/from-view.tsx deleted file mode 100644 index 6402f57aa..000000000 --- a/frontend/src/component/form-view/from-view.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from "react"; -import './form-view.scss'; -import Checkbox from "../checkbox/checkbox"; -import TagSelect from "../tag-select/tags-select"; -import MapEditor from "../map-editor/map-editor"; -import TagInput from "../tag-input/tag-input"; -import ScheduleEditor from "../schedule-editor/schedule-editor"; -import DatePicker from "../date-picker/date-picker"; -import {genUuid} from "../../utils/uuid"; -import {getIconByName} from "../../icons/icons"; -import useTranslator from "../../hook/use-translator"; -import ProxySelect from "../proxy-select/proxy-select"; -// export const isNumber = (value: string): boolean => { -// return !isNaN(value as any); -// } - -export enum FormFieldType { - READONLY= 'readonly', - TEXT = 'text', - NUMBER = 'number', - MULTI_SELECT = 'multi_select', - SINGLE_SELECT = 'single_select', - CHECK = 'checkbox', - TAGS = 'tags', - MAP = 'map', - SCHEDULE = 'schedule', - DATE = 'date', - PROXY_TYPE = 'proxy', -} - -export type FormField = { - name: string, - label: string, - hint?: string, - validator?: (value: any) => boolean, - options?: { value: string, label: string }[], - fieldType: FormFieldType - action?: {icon: string, handler: () => any} -}; - -interface FormViewProps { - data: any; - fields: FormField[] -} - -export default function FormView(props: FormViewProps) { - const {data, fields} = props; - const uuid = useMemo(() => genUuid(), []); - const translate = useTranslator(); - const inputIds= useRef([]); - const [formData, setFormData] = useState(data || {}); - - useEffect(() => { - if (data) { - setFormData(data); - inputIds.current.forEach(([id, fieldName])=> { - let elem: any = document.getElementById(id); - if (elem) { - const field = elem.dataset.field; - if (field) { - elem.value = data[field] ?? ''; - } - } - }); - } - }, [data]) - - const handleFieldAction = useCallback((evt: any) => { - const field = evt.target.dataset.field; - for (const fieldDef of fields) { - if (fieldDef.name === field) { - const value = fieldDef.action.handler(); - if (data) { - data[field] = value; - } - setFormData((prevData: any) => ({...prevData, [field]: value})); - for (const [id, fieldName] of inputIds.current) { - if (fieldName === field) { - let elem: any = document.getElementById(id); - if (elem) { - elem.value = value ?? ''; - } - break; - } - } - return; - } - } - }, [data, fields]); - - const handleInputValueChange = useCallback((evt: any) => { - const field = evt.target.dataset.field; - let value = evt.target.value; - for (const fieldDef of fields) { - if (fieldDef.name === field) { - if (fieldDef.fieldType === FormFieldType.NUMBER) { - if (value == null || value.trim() == '') { - value = undefined; - } else { - value = parseInt(value); - } - } - } - } - - if (data) { - data[field] = value; - } - setFormData((prevData: any) => ({...prevData, [field]: value})); - }, [data, fields]); - - const handleChange = useCallback((field: string, value: any) => { - if (data) { - data[field] = value; - } - setTimeout(()=> { - setFormData((prevData: any) => ({...prevData, [field]: value})); - }, 0); - }, [data]); - - const getFieldInput = useCallback((field: FormField) => { - switch (field.fieldType) { - case FormFieldType.READONLY: - return {formData?.[field.name]}; - case FormFieldType.CHECK: - return - case FormFieldType.MULTI_SELECT: - return - case FormFieldType.SINGLE_SELECT: - return - case FormFieldType.PROXY_TYPE: { - return - } - case FormFieldType.MAP: - return
- case FormFieldType.TAGS: - return - case FormFieldType.SCHEDULE: - return - case FormFieldType.DATE: - return - case FormFieldType.NUMBER: - case FormFieldType.TEXT: - default: { - const input_id = uuid + field.name; - inputIds.current.push([input_id, field.name]); - return ; - } - } - }, [uuid, formData, data, inputIds, handleChange, handleInputValueChange]); - - return
-
- { - fields.map(field => -
-
- - {field.hint && {getIconByName('Help')}} -
-
- {getFieldInput(field)} - {field.action ? : null} -
-
- ) - } -
-
-} diff --git a/frontend/src/component/input-field/input-field.scss b/frontend/src/component/input-field/input-field.scss deleted file mode 100644 index 8900d7beb..000000000 --- a/frontend/src/component/input-field/input-field.scss +++ /dev/null @@ -1,68 +0,0 @@ -@use "../../scss/common"; - -.input-field { - display: flex; - position: relative; - flex-flow: row nowrap; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - font-size: inherit; - min-width: 0; - max-width: 100%; - width: auto; - box-sizing: border-box; - background-color: inherit; - - &__content { - display: flex; - flex-flow: row nowrap; - flex: 1 1 0; - overflow: hidden; - border-radius: var(--border-radius); - font-size: inherit; - user-select: none; - box-sizing: border-box; - min-width: 0; - } - - &:focus-within { - border: 1px solid var(--hover-border-color); - } - - label { - position: absolute; - color: var(--label-color); - transform: translate(6px, -0.8rem); - background-color: inherit; - border-radius: var(--border-radius); - padding: 0 6px; - font-size: 1.1rem !important; - } - - //input { - // color: var(--text-color); - // border: none !important; - // flex: 1 1 0; - // height: 100%; - // width: 100%; - // min-width: 0; - // font-size: 1.1rem !important; - // outline: none !important; - // padding: 0 4px 0 12px; - // box-sizing: border-box; - //} - - button { - @include common.iconButton(); - border: none !important; - border-radius: 0 !important; - flex-shrink: 0; - &:last-child { - border-bottom-right-radius: var(--border-radius); - border-top-right-radius: var(--border-radius); - } - &:not(:last-child) { - border-right: 1px solid var(--card-background-color) !important; - } - } -} diff --git a/frontend/src/component/input-field/input-field.tsx b/frontend/src/component/input-field/input-field.tsx deleted file mode 100644 index 9ae365b82..000000000 --- a/frontend/src/component/input-field/input-field.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, {ReactNode} from "react"; -import './input-field.scss'; - -interface InputFieldProps { - label: string; - children: ReactNode; -} - -export default function InputField(props: InputFieldProps) { - const {label, children} = props; - return
- -
- {children} -
-
; - -} \ No newline at end of file diff --git a/frontend/src/component/loading-indicator/loading-indicator.scss b/frontend/src/component/loading-indicator/loading-indicator.scss deleted file mode 100644 index 301d60f5c..000000000 --- a/frontend/src/component/loading-indicator/loading-indicator.scss +++ /dev/null @@ -1,37 +0,0 @@ -.loading-bar-placeholder, .loading-bar-container { - width: 100%; // Full width of the container - height: 5px; // Thin height for the container - min-height: 5px; - max-height: 5px; -} - -// Container for the loading bar -.loading-bar-container { - background-color: var(--spinner-color); // Light gray background for the container - border-radius: 10px; // Optional: rounded corners for the container - overflow: hidden; // Prevents overflow content from showing outside the container -} - -// The actual loading bar -.loading-bar { - height: 100%; // Full height of the loading bar (matches the container's height) - width: 100%; // Start the width of the loading bar at 100% - background-color: var(--spinner-highlight-color); - position: relative; - animation: loading 2s infinite ease-in-out; // Animation runs infinitely with easing - transform-origin: center center; // The animation will scale from the center of the element -} - -// The keyframes for the loading animation -@keyframes loading { - 0% { - transform: scaleX(0); // At the start, the bar is not visible (scale from the center) - } - 50% { - transform: scaleX(1); // The bar reaches full width, expanding to the edges - } - 100% { - transform: scaleX(0); // The bar contracts back to the center - } -} - diff --git a/frontend/src/component/loading-indicator/loading-indicator.tsx b/frontend/src/component/loading-indicator/loading-indicator.tsx deleted file mode 100644 index f082d2abc..000000000 --- a/frontend/src/component/loading-indicator/loading-indicator.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React, {ReactNode} from "react"; -import './loading-indicator.scss'; - -interface LoadingIndicatorProps { - loading: boolean; -} - -export default function LoadingIndicator(props: LoadingIndicatorProps): ReactNode { - if (!props.loading) { - return
; - } - return
-
-
; -} \ No newline at end of file diff --git a/frontend/src/component/login/login.scss b/frontend/src/component/login/login.scss deleted file mode 100644 index a13d81621..000000000 --- a/frontend/src/component/login/login.scss +++ /dev/null @@ -1,78 +0,0 @@ -@use '../../scss/theme'; -@use '../../scss/common'; - -.login-view { - display: flex; - flex-flow: column; - gap: 8px; - margin: 10% auto auto auto; - border: 1px solid var(--border-color); - border-top-left-radius: 6px; - border-top-right-radius: 6px; - overflow: hidden; - position: relative; - z-index: 1; - - &__header { - display: flex; - flex-flow: row; - align-items: center; - background-color: var(--card-background-color); - padding: 0 8px; - &-title { - color: var(--card-color); - padding: 8px; - font-size: 1.5rem; - } - &-logo { - overflow: hidden; - img, svg { - height: 1.5rem; - width: 1.5rem; - } - } - - } - - &__logo { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - svg { - height: 100%; - width: 100%; - color: color-mix(in srgb, var(--background-color) 80%, black 10%); - } - } - - &__form { - display: flex; - flex-flow: column; - gap: 20px; - padding: 8px; - width: 300px; - - //input { - // height: 2rem; - // border: none; - // border-radius: 6px; - // font-size: 1.4rem; - // background-color: var(--card-background-color); - // padding: 4px 8px; - //} - input[type="password"] { - caret-color: var(--card-color); - } - - button { - @include common.textButton(); - } - - .error-text { - color: var(--attention-color); - font-size: 1rem; - } - } -} \ No newline at end of file diff --git a/frontend/src/component/login/login.tsx b/frontend/src/component/login/login.tsx deleted file mode 100644 index 8dd5badcf..000000000 --- a/frontend/src/component/login/login.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, {JSX, useCallback, useMemo, useRef, useState} from 'react'; -import './login.scss'; -import {useServices} from "../../provider/service-provider"; -import {first} from "rxjs/operators"; -import {getIconByName} from "../../icons/icons"; - -const checkUserPwd = (username: string, password: string) => username.trim().length > 0 && password.trim().length > 0; - -export default function Login(): JSX.Element { - - const usernameRef = useRef(undefined); - const passwordRef = useRef(undefined); - const services = useServices(); - const appTitle = useMemo(() => services.config().getUiConfig().app_title ?? 'tuliprox', [services]); - const appLogo = useMemo(() => { - let logo = services.config().getUiConfig().app_logo; - if (logo) { - return logo; - } else { - return getIconByName('Logo'); - } - }, [services]); - const [authorized, setAuthorized] = useState(true); - - const handleLogin = useCallback(() => { - const username = usernameRef.current.value; - const password = passwordRef.current.value; - services.auth().authenticate(username, password).pipe(first()).subscribe({ - next: (auth) => setAuthorized(auth), - error: () => setAuthorized(false) - }); - }, [services]); - - const handleKeyDown = useCallback((event: any) => { - if (event.code === 'Enter') { - if (checkUserPwd(usernameRef.current.value, passwordRef.current.value)) { - handleLogin(); - } - } - }, [handleLogin]); - - - return <> -
{getIconByName('Logo')}
-
-
-
{appLogo}
-
Login to {appTitle}
-
-
-
- - - - Failed to login -
-
-
- - -} \ No newline at end of file diff --git a/frontend/src/component/main-config-view/main-config-view.scss b/frontend/src/component/main-config-view/main-config-view.scss deleted file mode 100644 index f9781341c..000000000 --- a/frontend/src/component/main-config-view/main-config-view.scss +++ /dev/null @@ -1,69 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - -@media (min-width: common.$media-size-mobile) { - .main-config__form, .reverse-proxy__form { - .form-view__col-label { - min-width: 200px !important; - max-width: 200px !important; - width: 200px !important; - } - } -} - -.main-config { - - @include preferences.prefsRoot(); - @include preferences.prefsToolbar(); - @include preferences.prefsForm(); - - &__content-form { - @include common.container_vert_no_padding(); - } - - &__content { - @include common.container_vert_no_padding(flex-start); - align-items: stretch; - overflow-x: hidden; - overflow-y: auto; - - &-form { - &__section-title { - color: var(--title-label-color); - font-weight: bold; - border-bottom: 1px solid var(--border-color); - padding-bottom: 4px; - margin: 8px 0; - } - } - - &-help { - color: var(--title-label-color); - &-img { - max-width: 360px; - height: auto; - fill: currentColor; - } - - pre { - display: inline; - } - } - } - - &__restart { - @include common.container_horz_no_padding(); - border-top: 1px solid var(--border-color); - padding-top: 12px; - margin-top: 12px; - justify-content: center; - align-items: center; - color: var(--label-color); - - &-warn-icon { - color: var(--warn-color); - } - } - -} - diff --git a/frontend/src/component/main-config-view/main-config-view.tsx b/frontend/src/component/main-config-view/main-config-view.tsx deleted file mode 100644 index b91fe782b..000000000 --- a/frontend/src/component/main-config-view/main-config-view.tsx +++ /dev/null @@ -1,455 +0,0 @@ -import React, {useCallback, useMemo, useState} from "react"; -import './main-config-view.scss'; -import ServerConfig, { - CacheConfig, IpCheckConfig, - LogConfig, - MessagingConfig, ProxyConfig, - PushoverConfig, - RateLimitConfig, - RestConfig, - ReverseProxyConfig, - ServerApiConfig, - ServerMainConfig, - StreamBufferConfig, - StreamConfig, - TelegramConfig, - VideoConfig, - VideoDownloadConfig, - WebAuthConfig, - WebUiConfig -} from "../../model/server-config"; -import {useSnackbar} from "notistack"; -import FormView, {FormFieldType} from "../form-view/from-view"; -import TabSet from "../tab-set/tab-set"; -import {getIconByName} from "../../icons/icons"; -import {useServices} from "../../provider/service-provider"; -import {MessageKind} from "../../model/message-kind"; -import useTranslator from "../../hook/use-translator"; - -const SCHEDULE_IMG_PATH = 'm 54.272427,0.5747185 0.02871,0.7255 -0.645988,-0.2615234 -0.108992,0.3271567 0.677887,0.1787243 -0.457242,0.6053405 0.287638,0.1762001 0.377489,-0.6255353 0.363135,0.6300793 L 55.0827,2.1549654 54.635028,1.5445761 55.3177,1.3658518 55.208707,1.0437439 l -0.638542,0.251426 0.03137,-0.7204514 z m 8.921531,0 0.02871,0.7255 -0.645987,-0.2615234 -0.108992,0.3271567 0.677886,0.1787243 -0.457243,0.6053405 0.287639,0.1762001 0.377488,-0.6255353 0.363137,0.6300793 0.287634,-0.1756953 -0.447668,-0.6103893 0.682673,-0.1787243 -0.108994,-0.3221079 -0.638545,0.251426 0.03138,-0.7204514 z m 8.921526,0 0.02872,0.7255 L 71.498218,1.0386951 71.389224,1.3658518 72.06711,1.5445761 71.609873,2.1499166 71.897507,2.3261167 72.274999,1.7005814 72.63813,2.3306607 72.92577,2.1549654 72.478097,1.5445761 73.16077,1.3658518 73.051775,1.0437439 72.41323,1.2951699 72.44461,0.5747185 Z m 8.921531,0 0.02872,0.7255 -0.645986,-0.2615234 -0.108994,0.3271567 0.677886,0.1787243 -0.457243,0.6053405 0.28764,0.1762001 0.377488,-0.6255353 0.363137,0.6300793 0.287634,-0.1756953 -0.447667,-0.6103893 0.682672,-0.1787243 -0.108995,-0.3221079 -0.638544,0.251426 0.03138,-0.7204514 z M 18.714978,0.85088306 C 18.323122,0.85941045 18.03431,1.0005694 17.848345,1.2734603 17.65638,1.555154 17.560176,1.9799877 17.560176,2.5482601 c 0,0.5536178 0.09827,0.975372 0.29508,1.2652073 0.196808,0.2882067 0.491652,0.4321698 0.883646,0.4321698 0.391995,0 0.687869,-0.1448595 0.8879,-0.4346943 0.201643,-0.2914634 0.302523,-0.7123218 0.302523,-1.2626828 0,-0.5633875 -0.0962,-0.986934 -0.288168,-1.2702559 -0.190351,-0.28495037 -0.486225,-0.42712114 -0.887899,-0.42712114 -0.01286,0 -0.02563,-2.74e-4 -0.03828,0 z m 8.921527,0 c -0.391847,0.008527 -0.680667,0.14968634 -0.866631,0.42257724 -0.191965,0.2816937 -0.288169,0.7065274 -0.288169,1.2747998 0,0.5536178 0.09827,0.975372 0.29508,1.2652073 0.196804,0.2882067 0.491663,0.4321698 0.883645,0.4321698 0.391994,0 0.687871,-0.1448595 0.8879,-0.4346943 0.201646,-0.2914634 0.302524,-0.7123218 0.302524,-1.2626828 0,-0.5633875 -0.0962,-0.986934 -0.288166,-1.2702559 -0.190352,-0.28495037 -0.486226,-0.42712114 -0.8879,-0.42712114 -0.01286,0 -0.02564,-2.74e-4 -0.03828,0 z m 8.903452,0 c -0.195279,0.004198 -0.369359,0.0403707 -0.522105,0.10753754 -0.162928,0.0716446 -0.28928,0.1721549 -0.379618,0.3024179 -0.08872,0.130263 -0.13345,0.2756268 -0.13345,0.435199 0,0.182368 0.05512,0.3440251 0.16482,0.4856861 0.109689,0.1416614 0.254769,0.2347258 0.435442,0.2786889 v 0.00958 c -0.198415,0.034194 -0.36117,0.1254658 -0.488609,0.2736402 -0.127439,0.1465454 -0.191405,0.3246105 -0.191405,0.5346584 0,0.3028615 0.102439,0.5398887 0.30731,0.7108587 0.206481,0.1709703 0.491959,0.2564749 0.85653,0.2564749 0.372636,0 0.659161,-0.087297 0.859187,-0.2615236 0.201644,-0.1742269 0.302526,-0.4107488 0.302526,-0.7103537 0,-0.2165608 -0.06292,-0.3964181 -0.188745,-0.5397074 C 37.437626,2.589133 37.270711,2.4991484 37.061002,2.4649546 v -0.00958 c 0.180671,-0.035823 0.326262,-0.124407 0.437568,-0.2660671 0.111314,-0.1416614 0.166946,-0.3047191 0.166946,-0.4887153 0,-0.1644571 -0.04315,-0.3111082 -0.130261,-0.4397429 -0.08712,-0.1286347 -0.212948,-0.2287537 -0.377488,-0.30039832 -0.164542,-0.0732729 -0.357463,-0.109557 -0.578464,-0.109557 -0.01321,0 -0.02632,-2.7917e-4 -0.03934,-1.2e-7 z m 5.989336,0 c -0.31295,0 -0.565145,0.081024 -0.757108,0.24385284 -0.191962,0.1612004 -0.304285,0.3805334 -0.336552,0.6573422 l 0.445546,0.0414 c 0.02096,-0.1839965 0.08753,-0.3266721 0.200442,-0.427626 0.114538,-0.1025819 0.263772,-0.1534808 0.447672,-0.1534808 0.206482,0 0.365606,0.048715 0.476912,0.1464126 0.111302,0.097698 0.166947,0.2367886 0.166947,0.4175286 0,0.1416615 -0.05667,0.2892082 -0.169606,0.4422671 C 42.890633,2.3716389 42.70603,2.5514947 42.449541,2.7582871 42.162399,2.989504 41.935168,3.1955095 41.7674,3.3762495 41.601245,3.555361 41.478524,3.7334248 41.399481,3.910908 v 0.2857572 h 2.274511 V 3.8402253 h -1.749748 c 0.05323,-0.1188645 0.143707,-0.2411582 0.271156,-0.3665363 0.129051,-0.1270066 0.359933,-0.3236602 0.692241,-0.5906993 0.243587,-0.1953945 0.426623,-0.3858895 0.549222,-0.5715142 0.1226,-0.1872532 0.18396,-0.3746686 0.18396,-0.5619218 0,-0.2865785 -0.0962,-0.5080948 -0.288168,-0.6644104 C 43.140689,0.9288277 42.872892,0.85088306 42.529293,0.85088306 Z m 2.950272,0 c -0.391855,0.008527 -0.680669,0.14968634 -0.866634,0.42257724 -0.191963,0.2816937 -0.288168,0.7065274 -0.288168,1.2747998 0,0.5536178 0.09827,0.975372 0.295079,1.2652073 0.196833,0.2882067 0.491653,0.4321698 0.883648,0.4321698 0.391994,0 0.687868,-0.1448595 0.887899,-0.4346943 0.201644,-0.2914634 0.302524,-0.7123218 0.302524,-1.2626828 0,-0.5633875 -0.0962,-0.986934 -0.288169,-1.2702559 -0.190351,-0.28495037 -0.486226,-0.42712114 -0.8879,-0.42712114 -0.01285,0 -0.02563,-2.74e-4 -0.03828,0 z M 36.579303,1.1780397 c 0.204869,0 0.361389,0.046531 0.469467,0.1393443 0.109726,0.091183 0.16429,0.2271962 0.16429,0.407936 0,0.3793908 -0.209553,0.5689898 -0.628974,0.5689898 -0.200029,0 -0.355503,-0.049611 -0.46681,-0.1489365 -0.111314,-0.099325 -0.166947,-0.239313 -0.166947,-0.4200528 0,-0.1791116 0.05459,-0.3151236 0.164287,-0.407936 0.111301,-0.092812 0.26627,-0.1393444 0.464687,-0.1393448 z m -17.826045,0.029282 c 0.26133,0 0.44853,0.1058869 0.56145,0.3175639 0.114538,0.210049 0.171732,0.5511707 0.171732,1.0233739 0,0.4526641 -0.05979,0.7889138 -0.179175,1.0087327 -0.119374,0.219819 -0.307087,0.3296811 -0.563578,0.3296811 -0.258104,0 -0.446861,-0.1067821 -0.566236,-0.3200883 -0.117759,-0.2149342 -0.176515,-0.5542635 -0.176515,-1.0183255 0,-0.4770882 0.05771,-0.8200019 0.173858,-1.0284225 0.11776,-0.2084209 0.310681,-0.3125153 0.578464,-0.3125153 z m 8.92153,0 c 0.261329,0 0.448527,0.1058869 0.56145,0.3175639 0.114527,0.210049 0.171731,0.5511707 0.171731,1.0233739 0,0.4526641 -0.05981,0.7889138 -0.179177,1.0087327 -0.119373,0.219819 -0.307083,0.3296811 -0.563576,0.3296811 -0.258098,0 -0.446862,-0.1067821 -0.566234,-0.3200883 -0.117761,-0.2149342 -0.176516,-0.5542635 -0.176516,-1.0183255 0,-0.4770882 0.05771,-0.8200019 0.173856,-1.0284225 0.117761,-0.2084209 0.310677,-0.3125153 0.578466,-0.3125153 z m 17.843056,0 c 0.26133,0 0.448532,0.1058869 0.56145,0.3175639 0.114527,0.210049 0.171732,0.5511707 0.171732,1.0233739 0,0.4526641 -0.05979,0.7889138 -0.179174,1.0087327 -0.119373,0.219819 -0.307087,0.3296811 -0.563576,0.3296811 -0.258104,0 -0.446863,-0.1067821 -0.566236,-0.3200883 -0.117759,-0.214934 -0.177048,-0.5542633 -0.177048,-1.0183253 0,-0.4770884 0.05824,-0.820002 0.174388,-1.0284227 0.117762,-0.2084207 0.310683,-0.3125152 0.578464,-0.3125153 z M 18.460305,2.2332205 v 0.6078649 h 0.568893 V 2.2332205 Z m 8.921528,0 v 0.6078649 h 0.568894 V 2.2332205 Z m 17.843058,0 v 0.6078649 h 0.568893 V 2.2332205 Z m -8.645588,0.3907702 c 0.227452,0 0.404254,0.052691 0.53008,0.1585296 0.125824,0.104211 0.188745,0.2539408 0.188745,0.4493354 0,0.2312165 -0.05824,0.4031214 -0.174389,0.5154733 -0.116147,0.1123486 -0.29295,0.1686271 -0.530082,0.1686271 -0.23552,0 -0.414396,-0.057171 -0.536994,-0.1711513 -0.120985,-0.1156078 -0.1813,-0.2830327 -0.1813,-0.5028519 0,-0.1856235 0.06188,-0.3349638 0.186086,-0.4473157 0.125825,-0.1139805 0.298466,-0.1706465 0.517854,-0.1706465 z m 2.533438,0.8426299 -0.40886,1.6165977 h 0.297737 l 0.752321,-1.6165977 z m -20.202639,2.1199544 -0.271155,0.00404 0.03615,2.9327967 H 15.74875 L 16.305947,7.9948129 13.886288,8.6511457 16.305947,9.307478 15.750876,8.7808974 h 3.199097 z m 61.925937,0 -0.04199,3.1943203 h 3.201222 L 83.440733,9.3074758 85.85986,8.6511434 83.440733,7.9948107 83.997399,8.523411 h -2.92581 l 0.03613,-2.9327972 z m -53.488233,0.00201 0.04042,8.831209 h -11.63734 l 0.555071,-0.526581 -2.419659,0.656332 2.419659,0.656333 -0.555071,-0.526581 H 27.6615 L 27.6211,5.5885835 Z m 12.523104,0 0.04041,14.643285 H 15.750881 l 0.555071,-0.526581 -2.419659,0.656332 2.419659,0.656334 -0.555071,-0.526581 h 24.43159 L 40.142057,5.588588 Z m 14.231381,0 -0.04042,21.443903 h 29.879153 l -0.556661,0.5286 2.419128,-0.656332 -2.419128,-0.656334 0.554539,0.52658 H 54.335166 l 0.03828,-21.1864125 z m 9.311777,0 -0.04042,14.902789 h 20.621599 l -0.554537,0.526581 2.419127,-0.656334 -2.419127,-0.656332 0.554537,0.526581 H 63.646927 L 63.68521,5.5885805 Z m 8.710985,0 -0.03984,9.090712 h 11.91008 l -0.554537,0.526581 2.419127,-0.656333 -2.419127,-0.656333 0.554537,0.526581 H 72.358492 l 0.03776,-8.831208 z M 1.4642066,7.0077877 c -0.3505704,0 -0.6212685,0.066373 -0.81186986,0.1989192 -0.19060142,0.1325459 -0.28604189,0.3204256 -0.28604189,0.5639411 0,0.2003604 0.0647252,0.3584814 0.19406186,0.4740741 C 0.68969335,8.358773 0.89991743,8.4436925 1.190925,8.4991769 l 0.1632245,0.03433 c 0.2910076,0.057027 0.4795501,0.1165706 0.5646403,0.1782194 0.086791,0.06165 0.1297293,0.14997 0.1297291,0.2655622 0,0.1248403 -0.059517,0.2219575 -0.1786433,0.2913107 -0.1191259,0.067815 -0.2891753,0.1019835 -0.5104093,0.1019835 -0.1650744,0 -0.3318295,-0.020007 -0.50030745,-0.060079 -0.16847802,-0.041612 -0.3394843,-0.103601 -0.5130677,-0.1852862 v 0.4397426 c 0.1837942,0.053945 0.35809515,0.09396 0.52316955,0.120159 0.1667762,0.027738 0.3269419,0.0414 0.4801038,0.0414 0.3675883,0 0.6557162,-0.070278 0.8650373,-0.2105313 C 2.425425,9.375735 2.531281,9.1854112 2.531281,8.9449783 2.531281,8.7338293 2.4603914,8.5698634 2.3191422,8.452729 2.1795945,8.334056 1.9434381,8.2423062 1.6098858,8.177574 L 1.448788,8.1452622 C 1.1969218,8.0944014 1.0292102,8.0412075 0.9458221,7.9857231 0.86243399,7.9286973 0.82087814,7.8511115 0.82087814,7.7524725 c 0,-0.1294635 0.0581358,-0.2265789 0.17385815,-0.2913107 0.11742401,-0.064731 0.29268161,-0.09744 0.52582791,-0.09744 0.1531619,0 0.3009987,0.015623 0.4439498,0.046448 0.142951,0.030825 0.2809036,0.07719 0.4136441,0.1388399 V 7.146627 C 2.2488212,7.1003892 2.1075739,7.0657419 1.9544121,7.0426234 1.8012502,7.0195053 1.6377898,7.0077877 1.4642066,7.0077877 Z m 3.0119464,0 c -0.428853,0 -0.7699091,0.1249335 -1.023477,0.3746145 -0.2518659,0.2481362 -0.3774901,0.583883 -0.3774901,1.0077226 0,0.4099686 0.1322134,0.7354814 0.3976937,0.9759161 0.2671823,0.240433 0.6290696,0.3604783 1.0851515,0.3604783 0.1820925,0 0.3616012,-0.017085 0.5385882,-0.050992 C 5.273606,9.641621 5.4465257,9.5923326 5.6150037,9.5276 V 9.1252175 c -0.1667761,0.080144 -0.3354446,0.1406714 -0.5056242,0.1807437 -0.168478,0.040073 -0.3427789,0.06008 -0.5231696,0.06008 -0.311429,0 -0.5547064,-0.076629 -0.7299916,-0.2292116 C 3.6826348,8.9827058 3.585281,8.7591954 3.5648599,8.4663605 h 2.159669 v -0.208007 c 0,-0.3853089 -0.1124446,-0.6893281 -0.3370828,-0.9128073 C 5.1645109,7.1205259 4.8607594,7.0077877 4.476153,7.0077877 Z m 3.2522641,0 c -0.445871,0 -0.7973428,0.1210289 -1.0543142,0.3630024 C 6.4188333,7.612764 6.291296,7.9451073 6.291296,8.3674055 c 0,0.4161339 0.1265809,0.7465106 0.3801488,0.9915672 0.2535678,0.2450565 0.5961119,0.3675465 1.0266667,0.3675465 0.1582705,0 0.3070578,-0.013665 0.4466082,-0.0414 C 8.2859688,9.6573783 8.4220055,9.6153957 8.5530473,9.559911 V 9.1671212 C 8.4203038,9.2333942 8.286605,9.2836652 8.1521633,9.317573 8.0194228,9.3499383 7.8852996,9.366041 7.7491522,9.366041 7.4445309,9.366041 7.2078427,9.2786786 7.0393649,9.1045179 6.8708865,8.9288169 6.7862866,8.6833589 6.7862866,8.3674055 c 0,-0.315953 0.0846,-0.5614377 0.2530783,-0.7355973 0.1684778,-0.1757005 0.405166,-0.2635427 0.7097873,-0.2635427 0.1361474,0 0.2702706,0.017085 0.4030111,0.050993 0.1344417,0.032366 0.2681405,0.081654 0.400884,0.1479268 V 7.1698512 C 8.4237077,7.1159083 8.2900085,7.0753881 8.1521633,7.0491868 8.0143177,7.0214452 7.8730697,7.0077877 7.7284171,7.0077877 Z m 84.1574529,0 c -0.40487,0 -0.726666,0.1322864 -0.966055,0.3968286 -0.237782,0.262909 -0.356758,0.6187343 -0.356758,1.0678024 0,0.4343718 0.125264,0.7792322 0.375899,1.0339763 0.252241,0.2547445 0.59396,0.3821877 1.02454,0.3821877 0.171908,0 0.341191,-0.018096 0.508284,-0.054021 0.167087,-0.035925 0.330083,-0.088429 0.489142,-0.1570148 V 9.251435 c -0.157452,0.084915 -0.316255,0.14889 -0.476916,0.1913465 -0.159059,0.042458 -0.324156,0.063614 -0.494456,0.063614 -0.294019,0 -0.523572,-0.081179 -0.689057,-0.2428431 C 91.13662,9.100255 91.044899,8.8634641 91.025619,8.5531987 h 2.038979 V 8.3330745 c 0,-0.4082436 -0.105874,-0.7310568 -0.317945,-0.9678383 C 92.536183,7.126822 92.248967,7.0077877 91.88587,7.0077877 Z m 2.853507,0 c -0.144594,0 -0.292912,0.016314 -0.445544,0.048973 -0.152633,0.031026 -0.310387,0.078183 -0.47266,0.1418685 v 0.4165187 c 0.134955,-0.075117 0.275413,-0.1315739 0.421619,-0.1691318 0.1462,-0.037559 0.296623,-0.056042 0.450864,-0.056042 0.242602,0 0.430754,0.056845 0.564107,0.1711518 0.134954,0.1126755 0.202571,0.2727444 0.202571,0.4801326 v 0.044429 h -0.622067 c -0.403263,0 -0.706719,0.078504 -0.910756,0.2352698 -0.202438,0.1567656 -0.30359,0.3890998 -0.30359,0.6977322 0,0.2645417 0.08071,0.4762245 0.242973,0.6346232 0.163879,0.1567653 0.384001,0.2352699 0.660342,0.2352699 0.218504,0 0.403518,-0.039252 0.554537,-0.1176344 0.151027,-0.080016 0.277339,-0.2035072 0.378561,-0.3700707 v 0.4165187 h 0.443417 V 8.252295 c 0,-0.4180416 -0.09644,-0.7302766 -0.289236,-0.9360312 -0.192797,-0.205755 -0.484723,-0.3084761 -0.875138,-0.3084761 z m 3.414958,0 c -0.213683,0 -0.39502,0.041035 -0.54444,0.1226835 -0.149413,0.080016 -0.271011,0.2030025 -0.364193,0.3695657 V 7.0739256 h -0.445551 v 2.7434701 h 0.445551 V 8.3719493 c 0,-0.3102626 0.06708,-0.5474426 0.200442,-0.712373 0.134955,-0.1665636 0.327294,-0.2499114 0.577929,-0.2499114 0.07071,0 0.135164,0.00702 0.193001,0.021708 0.05945,0.013065 0.113958,0.034725 0.163752,0.064119 l -0.0023,-0.4629673 c -0.045,-0.00816 -0.08587,-0.01389 -0.122816,-0.017166 -0.03694,-0.0049 -0.07103,-0.00758 -0.101558,-0.00758 z m -10.637247,0.066138 1.091532,2.6995466 -0.07976,0.2054822 c -0.08192,0.2188185 -0.15635,0.3577325 -0.223828,0.4165185 -0.06748,0.05878 -0.162341,0.08835 -0.28445,0.08835 h -0.260518 v 0.37714 h 0.35463 c 0.199218,0 0.358,-0.04983 0.476911,-0.149443 0.118889,-0.09961 0.241007,-0.313076 0.366328,-0.639671 L 90.143569,7.0739237 H 89.673567 L 88.830328,9.2196281 87.986561,7.0739256 Z M 4.4814697,7.3682655 c 0.2314446,0 0.4171176,0.069295 0.5566652,0.208007 0.1412491,0.1387081 0.2135204,0.3246508 0.2169239,0.5573779 l -1.6747805,0.00252 C 3.604103,7.8926553 3.6943361,7.7037924 3.8509016,7.569705 4.0091688,7.4356174 4.2193929,7.3682614 4.4814697,7.3682614 Z m 87.4097183,0.021708 c 0.218504,0 0.393553,0.07366 0.525295,0.2206287 0.133382,0.1469685 0.201515,0.3436152 0.204699,0.5901945 l -1.581208,0.00252 c 0.0225,-0.2580102 0.107927,-0.4582234 0.255734,-0.6002923 0.149419,-0.1420694 0.348058,-0.2130557 0.59548,-0.2130557 z m 3.127853,1.0481129 h 0.441293 v 0.1004701 c 0,0.2939352 -0.07496,0.5298348 -0.224372,0.7078292 -0.147809,0.1763617 -0.344875,0.2645527 -0.59069,0.2645524 -0.178338,6e-7 -0.319313,-0.046265 -0.423748,-0.139344 -0.102823,-0.094713 -0.154187,-0.2221561 -0.154187,-0.3821874 0,-0.200856 0.06862,-0.3428298 0.206827,-0.4261113 0.138162,-0.083282 0.386594,-0.125208 0.744877,-0.125209 z M 5.6479675,11.980768 v 0.538699 h 0.4700018 v -0.538699 z m 83.9633915,0.295856 v 1.484321 c -0.09319,-0.163298 -0.211636,-0.284116 -0.354629,-0.362497 -0.141381,-0.08002 -0.311716,-0.120159 -0.51094,-0.120159 -0.326146,0 -0.591867,0.132285 -0.797518,0.396829 -0.204037,0.264538 -0.306245,0.612463 -0.306245,1.043567 0,0.431106 0.102208,0.779027 0.306245,1.043569 0.205651,0.264542 0.471372,0.39683 0.797518,0.39683 0.199224,0 0.369559,-0.03926 0.51094,-0.11763 0.142993,-0.08001 0.261446,-0.201723 0.354629,-0.365022 v 0.411471 h 0.44342 v -3.811279 z m 12.837321,0 c -0.30365,0 -0.52534,0.07009 -0.66513,0.210531 -0.13977,0.138801 -0.20948,0.361063 -0.20948,0.66643 v 0.19084 h -0.42428 v 0.350382 h 0.42428 v 2.39309 h 0.44608 v -2.39309 h 0.72999 v -0.350382 h -0.72999 v -0.242337 c 0,-0.169826 0.0303,-0.287582 0.0914,-0.352906 0.0627,-0.06532 0.17325,-0.09794 0.3323,-0.09794 h 0.42428 v -0.374614 z m 12.4524,0 v 3.811273 h 0.44608 v -1.33993 l 1.37119,1.33993 h 0.57846 l -1.49189,-1.459583 1.43127,-1.283889 h -0.56622 l -1.32281,1.18342 V 12.276624 Z M 1.8906111,12.926393 c -0.1897572,0.0044 -0.3551242,0.04229 -0.4955225,0.11309 -0.1497583,0.07552 -0.278252,0.192644 -0.3854653,0.351392 V 12.988491 H 0.53749465 v 2.589485 H 1.0096233 v -1.463621 c 0,-0.251222 0.07089,-0.449836 0.2121388,-0.596253 0.1412493,-0.147959 0.3315986,-0.222144 0.5715522,-0.222144 0.2025141,0 0.352264,0.06002 0.4492665,0.180239 0.097002,0.118676 0.1456793,0.303154 0.1456793,0.552834 v 1.548945 h 0.4721285 v -1.463621 c 0,-0.252763 0.07089,-0.452359 0.2121389,-0.598777 0.1412491,-0.146417 0.3335118,-0.21962 0.5768689,-0.21962 0.1991102,0 0.3469472,0.06002 0.4439495,0.180239 0.097002,0.120216 0.1456793,0.304695 0.1456793,0.552834 v 1.548945 h 0.4721287 v -1.56308 c 0,-0.348321 -0.080773,-0.61623 -0.2424445,-0.80426 -0.1616707,-0.189571 -0.3912379,-0.284243 -0.6890525,-0.284243 -0.2212337,0 -0.4134963,0.04538 -0.5768686,0.136316 -0.1633738,0.09093 -0.3041954,0.231968 -0.4216194,0.42308 C 2.7113953,13.303925 2.6037326,13.164826 2.457378,13.069272 2.3127252,12.973707 2.1365109,12.926393 1.9288917,12.926393 c -0.012871,0 -0.025631,-2.85e-4 -0.038281,0 z m 6.5513156,0 c -0.1839736,0.0045 -0.3476798,0.04287 -0.4912687,0.115112 -0.1514601,0.07706 -0.2832488,0.193704 -0.3955675,0.34937 V 12.988491 H 7.0824304 v 2.589485 h 0.4726601 v -1.463621 c 0,-0.252763 0.073227,-0.452359 0.2195822,-0.598777 0.1463578,-0.146417 0.3461603,-0.21962 0.5997311,-0.21962 0.2110231,0 0.3692756,0.06102 0.4747868,0.182763 0.1055124,0.121759 0.1584397,0.305253 0.1584397,0.55031 v 1.548945 H 9.477101 v -1.56308 c 0,-0.357567 -0.084069,-0.627923 -0.2525466,-0.811328 -0.1684778,-0.18495 -0.4169632,-0.277175 -0.7454102,-0.277175 -0.012447,0 -0.024952,-3e-4 -0.037219,0 z m -2.7939592,0.0621 v 2.589484 h 0.4700018 v -2.589485 z m 86.2671455,0.289796 c -0.1446,0 -0.293445,0.01631 -0.446077,0.04897 -0.152633,0.03103 -0.309855,0.07869 -0.472127,0.142375 v 0.416014 c 0.134954,-0.07513 0.275413,-0.131067 0.42162,-0.168628 0.1462,-0.03756 0.296623,-0.05654 0.450856,-0.05654 0.242609,0 0.430233,0.05735 0.563581,0.171656 0.134955,0.112675 0.202565,0.272743 0.202565,0.480131 v 0.04392 h -0.621528 c -0.403268,0 -0.707246,0.0785 -0.91129,0.23527 -0.202437,0.156765 -0.30359,0.389099 -0.30359,0.697731 0,0.264541 0.08124,0.476225 0.243507,0.634622 0.16388,0.156768 0.384001,0.235272 0.660342,0.235272 0.218503,0 0.403518,-0.03926 0.554538,-0.11763 0.151027,-0.08001 0.276805,-0.203507 0.378021,-0.37007 v 0.416519 h 0.443422 v -1.565108 c 0,-0.418041 -0.09644,-0.729772 -0.289235,-0.935525 -0.192792,-0.205755 -0.484196,-0.308982 -0.874605,-0.308982 z m 7.613074,0 c -0.387196,0 -0.691185,0.127443 -0.911295,0.382188 -0.218503,0.254744 -0.327533,0.607509 -0.327509,1.058209 -2.9e-5,0.449068 0.109006,0.801834 0.327509,1.058211 0.22011,0.254745 0.524099,0.382188 0.911295,0.382188 0.385596,0 0.688523,-0.12744 0.908633,-0.382188 0.22012,-0.256377 0.33018,-0.609143 0.33018,-1.058211 0,-0.4507 -0.11006,-0.803465 -0.33018,-1.058209 -0.22011,-0.254745 -0.523037,-0.382188 -0.908633,-0.382188 z m 10.446913,0 c -0.40487,0 -0.72667,0.132286 -0.96607,0.396829 -0.23777,0.262907 -0.35675,0.61924 -0.35675,1.068307 0,0.434372 0.12527,0.778728 0.3759,1.033472 0.25224,0.254745 0.59343,0.382188 1.024,0.382188 0.17193,0 0.34174,-0.01809 0.50883,-0.05401 0.16708,-0.03592 0.33008,-0.08793 0.48913,-0.156512 v -0.426616 c -0.15744,0.08492 -0.31678,0.148891 -0.47744,0.191347 -0.15906,0.04246 -0.32363,0.06362 -0.49393,0.06362 -0.29401,0 -0.52357,-0.08066 -0.68905,-0.242338 -0.16386,-0.163298 -0.25559,-0.400089 -0.27489,-0.710354 h 2.03899 v -0.220637 c 0,-0.408243 -0.1064,-0.730551 -0.31847,-0.967332 -0.21048,-0.238415 -0.49715,-0.357954 -0.86025,-0.357954 z m 3.03693,0 c -0.40488,0 -0.7272,0.132286 -0.96659,0.396829 -0.23779,0.262907 -0.35675,0.61924 -0.35675,1.068307 0,0.434372 0.12525,0.778728 0.37589,1.033472 0.25225,0.254745 0.59396,0.382188 1.02454,0.382188 0.17191,0 0.3412,-0.01809 0.50828,-0.05401 0.16709,-0.03592 0.33008,-0.08793 0.48915,-0.156512 v -0.426616 c -0.15746,0.08492 -0.31625,0.148891 -0.47692,0.191347 -0.15906,0.04246 -0.32414,0.06362 -0.49445,0.06362 -0.29401,0 -0.52358,-0.08066 -0.68906,-0.242338 -0.16387,-0.163298 -0.25557,-0.400089 -0.27487,-0.710354 h 2.03897 v -0.220637 c 0,-0.408243 -0.10585,-0.730551 -0.31794,-0.967332 -0.21047,-0.238415 -0.49715,-0.357954 -0.86025,-0.357954 z m -19.337603,0.06614 1.091537,2.699547 -0.07923,0.205484 c -0.08193,0.218818 -0.156889,0.357732 -0.224367,0.416518 -0.06749,0.05879 -0.162342,0.08835 -0.284449,0.08835 h -0.259993 v 0.377138 h 0.354101 c 0.199219,0 0.358023,-0.04983 0.476911,-0.149445 0.118894,-0.09962 0.241007,-0.312572 0.366328,-0.639167 l 1.185636,-2.998431 h -0.469469 l -0.843773,2.145704 -0.843239,-2.145704 z m 10.875963,0 0.70607,2.743472 h 0.52317 l 0.58325,-2.248698 0.5806,2.248698 h 0.52317 l 0.70606,-2.743472 h -0.44342 l -0.55188,2.14116 -0.55453,-2.14116 h -0.52264 l -0.55189,2.14116 -0.55453,-2.14116 z M 88.854249,13.65543 c 0.23457,0 0.419584,0.09481 0.554539,0.284241 0.13496,0.187792 0.202571,0.447524 0.202571,0.779017 0,0.331495 -0.06761,0.592116 -0.202571,0.781542 -0.134955,0.187792 -0.319969,0.281718 -0.554539,0.281718 -0.234568,0 -0.419049,-0.09393 -0.554003,-0.281718 -0.133348,-0.189428 -0.19991,-0.450049 -0.19991,-0.781544 0,-0.331493 0.06656,-0.591225 0.19991,-0.779017 0.134954,-0.189424 0.319435,-0.284242 0.554003,-0.284242 z m 10.673938,0.0051 c 0.236182,0 0.423279,0.09571 0.561453,0.286767 0.13817,0.189426 0.20732,0.446481 0.20735,0.771444 0,0.326595 -0.0691,0.584543 -0.20735,0.773966 -0.138174,0.189423 -0.325271,0.284243 -0.561453,0.284243 -0.239389,0 -0.428069,-0.09393 -0.566236,-0.281718 -0.13656,-0.189425 -0.205227,-0.448262 -0.205227,-0.776491 0,-0.328228 0.06918,-0.586177 0.207356,-0.773969 0.138167,-0.189425 0.326325,-0.284242 0.564107,-0.284242 z m 10.451693,0 c 0.2185,0 0.39408,0.07366 0.52583,0.220629 0.13324,0.146967 0.20137,0.343615 0.20469,0.590194 l -1.5812,0.0025 c 0.0225,-0.258009 0.10793,-0.458222 0.25574,-0.600291 0.14943,-0.142069 0.34753,-0.213055 0.59494,-0.213055 z m 3.03693,0 c 0.21851,0 0.39356,0.07366 0.5253,0.220629 0.13328,0.146967 0.20137,0.343615 0.2047,0.590194 l -1.58068,0.0025 c 0.0225,-0.258009 0.1074,-0.458222 0.25521,-0.600291 0.14941,-0.142069 0.34805,-0.213055 0.59547,-0.213055 z m -20.822039,1.048618 h 0.44076 v 0.100467 c 0,0.293935 -0.07441,0.529835 -0.223834,0.70783 -0.147812,0.17636 -0.344873,0.264551 -0.59069,0.264551 -0.178338,0 -0.319846,-0.04676 -0.424281,-0.139844 -0.102822,-0.09471 -0.154181,-0.222157 -0.154181,-0.382187 0,-0.200858 0.06915,-0.34283 0.20735,-0.426112 0.138168,-0.08328 0.386599,-0.124702 0.744876,-0.124702 z M 0.53749465,17.898916 v 3.597209 H 1.0096233 v -1.463164 c 0,-0.252764 0.073228,-0.452361 0.2195824,-0.598778 0.1463546,-0.146415 0.3466943,-0.219619 0.6002624,-0.219619 0.2110231,0 0.3692755,0.0605 0.4747871,0.182259 0.1055113,0.121755 0.1579079,0.305253 0.1579079,0.550309 v 1.548945 H 2.9321649 V 19.9335 c 0,-0.357565 -0.084599,-0.628426 -0.2530778,-0.811833 -0.1684781,-0.184948 -0.416963,-0.277174 -0.7454104,-0.277174 -0.1991104,0 -0.3747928,0.03855 -0.5279546,0.11562 -0.15146,0.07707 -0.2837802,0.193201 -0.3960988,0.348867 V 17.898869 Z M 100.70903,18.547124 v 3.811273 h 0.44555 v -1.55046 c 0,-0.267807 0.0691,-0.478984 0.20736,-0.634119 0.13817,-0.155128 0.32685,-0.232744 0.56623,-0.232744 0.19923,0 0.34913,0.06437 0.44873,0.193366 0.0996,0.128999 0.14941,0.323483 0.14941,0.583126 v 1.640831 h 0.44341 v -1.655473 c 0,-0.378851 -0.0796,-0.665472 -0.23871,-0.859796 -0.15905,-0.195957 -0.39387,-0.293835 -0.70395,-0.293835 -0.18797,0 -0.35412,0.04054 -0.4987,0.122175 -0.143,0.08165 -0.26773,0.20514 -0.37378,0.37007 v -1.494419 z m -1.925719,0.289291 v 0.779016 h -0.325386 v 0.349877 h 0.325386 v 1.489369 c 0,0.342926 0.06499,0.579715 0.195117,0.710353 0.13017,0.129001 0.36652,0.193367 0.708731,0.193367 h 0.455641 v -0.377139 h -0.455641 c -0.184764,0 -0.307933,-0.03174 -0.368984,-0.09541 -0.05944,-0.06369 -0.08879,-0.207442 -0.08879,-0.43116 v -1.489366 h 0.913425 V 19.61544 h -0.913422 v -0.779016 z m -93.8068505,0.0081 c -0.4101334,0 -0.7313147,0.120045 -0.9644609,0.360479 -0.2314446,0.240432 -0.3471848,0.57376 -0.3471848,0.999139 0,0.42384 0.1157402,0.756662 0.3471848,0.998636 0.2331462,0.240433 0.5543275,0.360478 0.9644609,0.360478 0.4084316,0 0.7291879,-0.120045 0.962334,-0.360478 0.2331461,-0.241974 0.3498433,-0.574796 0.3498433,-0.998636 0,-0.425379 -0.1166972,-0.758707 -0.3498433,-0.999139 C 5.7056484,18.964535 5.3848921,18.84449 4.9764605,18.84449 Z m 6.8208755,0 c -0.226339,0 -0.418601,0.03855 -0.576868,0.11562 -0.158269,0.07552 -0.286762,0.191661 -0.385466,0.348867 v -0.401878 h -0.472129 v 2.58898 h 0.472129 V 20.13245 c 0,-0.292836 0.07089,-0.517329 0.212138,-0.672993 0.142952,-0.157206 0.347012,-0.235776 0.612493,-0.235776 0.07488,0 0.142898,0.0069 0.204164,0.0207 0.06296,0.0123 0.121105,0.03234 0.173857,0.06008 l -0.0027,-0.436713 c -0.04765,-0.0078 -0.09111,-0.01305 -0.130261,-0.01616 -0.03913,-0.0046 -0.07506,-0.007 -0.107404,-0.007 z m -4.7787067,0.0626 v 1.567121 c 0,0.356025 0.085556,0.626884 0.2557365,0.811833 0.1701796,0.184949 0.4190896,0.277174 0.7475365,0.277174 0.1991105,0 0.3739434,-0.03757 0.5237012,-0.113085 0.15146,-0.07706 0.2842052,-0.194187 0.3982257,-0.35139 V 21.49611 H 9.4132997 V 18.907131 H 8.9438292 v 1.467662 c 0,0.252762 -0.074184,0.452361 -0.222241,0.598778 -0.1463543,0.146415 -0.3461632,0.219617 -0.5997304,0.219617 -0.2110231,0 -0.3698074,-0.0605 -0.4753188,-0.182257 -0.1055113,-0.1233 -0.1579079,-0.307778 -0.1579079,-0.552834 V 18.907131 Z M 4.9764605,19.20499 c 0.2501643,0 0.4485909,0.09028 0.5949456,0.27061 0.1463577,0.178784 0.2195824,0.421824 0.2195824,0.728529 0,0.30825 -0.073225,0.551766 -0.2195824,0.73055 -0.1463547,0.178783 -0.3447813,0.268086 -0.5949456,0.268086 -0.253568,0 -0.4533763,-0.08883 -0.5997309,-0.266067 -0.1446529,-0.178784 -0.216924,-0.42278 -0.216924,-0.732569 0,-0.309787 0.073227,-0.553306 0.2195822,-0.730548 0.1463546,-0.178783 0.3452066,-0.268591 0.5970727,-0.268591 z m 84.1191735,0.344293 c -0.17915,0.0046 -0.335333,0.04464 -0.467874,0.119655 -0.141387,0.08001 -0.26245,0.204399 -0.363672,0.372594 v -0.426111 h -0.446073 v 2.742966 h 0.446073 v -1.55046 c 0,-0.266175 0.06656,-0.476461 0.199915,-0.631594 0.133376,-0.156765 0.313113,-0.235269 0.539649,-0.235269 0.191191,0 0.3327,0.06347 0.424276,0.19084 0.09159,0.125745 0.137176,0.32111 0.137176,0.585651 v 1.640832 h 0.446078 v -1.55046 c 0,-0.267807 0.06655,-0.478984 0.199908,-0.634119 0.133348,-0.155128 0.315218,-0.232744 0.544968,-0.232744 0.187978,0 0.327387,0.06347 0.418964,0.19084 0.09158,0.127381 0.137704,0.322742 0.137704,0.585651 v 1.640832 h 0.445544 v -1.655473 c 0,-0.369053 -0.07599,-0.653504 -0.228625,-0.852728 -0.152628,-0.200856 -0.369611,-0.300903 -0.650772,-0.300903 -0.208857,0 -0.390728,0.04804 -0.544961,0.14439 -0.15424,0.09633 -0.28684,0.245837 -0.397701,0.448326 -0.06588,-0.192691 -0.167545,-0.340014 -0.305713,-0.441258 -0.13656,-0.101247 -0.302701,-0.151461 -0.498712,-0.151461 -0.01213,0 -0.02418,-3e-4 -0.03613,0 z m 4.556997,0 c -0.37004,0.0076 -0.661375,0.134895 -0.874605,0.381683 -0.218505,0.254745 -0.328049,0.607509 -0.328049,1.058211 0,0.449068 0.109539,0.801832 0.328049,1.05821 0.220109,0.254745 0.52356,0.382188 0.910756,0.382188 0.385596,0 0.6885,-0.127441 0.908639,-0.382188 0.220105,-0.256378 0.330171,-0.609142 0.330171,-1.05821 0,-0.450702 -0.110072,-0.803466 -0.330171,-1.058211 -0.220139,-0.254744 -0.523043,-0.381683 -0.908639,-0.381683 -0.01207,0 -0.02424,-2.49e-4 -0.03614,0 z m 3.276187,0 c -0.17368,0.0048 -0.328059,0.04563 -0.463622,0.122175 -0.142987,0.08165 -0.267728,0.20514 -0.373764,0.37007 v -0.426111 h -0.445544 v 2.742966 h 0.445544 v -1.55046 c 0,-0.267807 0.06916,-0.478985 0.207356,-0.634119 0.138162,-0.155129 0.326841,-0.232745 0.56623,-0.232745 0.199225,0 0.348592,0.06436 0.448207,0.193365 0.09961,0.129 0.149403,0.323484 0.149403,0.583127 v 1.640832 h 0.443416 v -1.655474 c 0,-0.37885 -0.07914,-0.665472 -0.238196,-0.859795 -0.159052,-0.195957 -0.393854,-0.293835 -0.70394,-0.293835 -0.01178,0 -0.02348,-3e-4 -0.03509,0 z m -3.240036,0.381683 c 0.236176,0 0.423284,0.09571 0.561451,0.286767 0.138175,0.189424 0.207357,0.446481 0.207357,0.771444 0,0.326595 -0.06918,0.584541 -0.207357,0.773966 -0.138167,0.189425 -0.325275,0.284244 -0.561451,0.284244 -0.239389,0 -0.428063,-0.09393 -0.566231,-0.281719 -0.136566,-0.189424 -0.205227,-0.448263 -0.205227,-0.776491 0,-0.328229 0.06918,-0.586176 0.20735,-0.773969 0.138174,-0.189424 0.326325,-0.284242 0.564108,-0.284242 z m -4.077423,4.886648 v 1.484828 c -0.09319,-0.163298 -0.211636,-0.28462 -0.354629,-0.363004 -0.141381,-0.08001 -0.311716,-0.119654 -0.51094,-0.119654 -0.326146,0 -0.591867,0.132284 -0.797518,0.39683 -0.204037,0.26454 -0.306245,0.612461 -0.306245,1.043568 0,0.431079 0.102208,0.778521 0.306245,1.043063 0.205651,0.264544 0.471372,0.396831 0.797518,0.396831 0.199224,0 0.369559,-0.03926 0.51094,-0.11763 0.142993,-0.08001 0.261446,-0.201219 0.354629,-0.364517 V 28.6294 h 0.44342 v -3.81178 z m 12.837321,0 c -0.30365,0 -0.52534,0.07061 -0.66513,0.211038 -0.13977,0.138794 -0.20948,0.360558 -0.20948,0.665923 v 0.191346 h -0.42428 v 0.350381 h 0.42428 v 2.393092 h 0.44608 V 26.23631 h 0.72999 v -0.350381 h -0.72999 v -0.242841 c 0,-0.169834 0.0303,-0.287086 0.0914,-0.352402 0.0627,-0.06532 0.17325,-0.09796 0.3323,-0.09796 h 0.42428 v -0.37512 z m 15.23359,0 v 3.81178 h 0.44608 v -1.550461 c 0,-0.267809 0.0686,-0.479488 0.20682,-0.634624 0.13817,-0.155117 0.32738,-0.232745 0.56676,-0.232745 0.19923,0 0.34861,0.06436 0.4482,0.193365 0.0996,0.129 0.14942,0.323485 0.14942,0.583126 v 1.641339 h 0.44341 v -1.655979 c 0,-0.378848 -0.0796,-0.665473 -0.23873,-0.859797 -0.15905,-0.195956 -0.39385,-0.293834 -0.70393,-0.293834 -0.18799,0 -0.35413,0.04054 -0.49872,0.122175 -0.14301,0.08164 -0.26719,0.205139 -0.37323,0.370069 V 24.81761 Z m -1.92572,0.289292 v 0.779015 h -0.32486 v 0.350381 h 0.32486 v 1.488865 c 0,0.342926 0.0655,0.579717 0.19565,0.710356 0.13014,0.128999 0.36597,0.193871 0.7082,0.193871 h 0.45564 v -0.377645 h -0.45564 c -0.18476,0 -0.30739,-0.03174 -0.36846,-0.09542 -0.0595,-0.06369 -0.0892,-0.207442 -0.0892,-0.431161 v -1.488865 h 0.91341 v -0.350381 h -0.91341 v -0.779005 z m -23.841437,0.71288 c -0.1446,0 -0.293445,0.01632 -0.446077,0.04897 -0.152633,0.03104 -0.309855,0.07818 -0.472127,0.14187 v 0.41652 c 0.134954,-0.07512 0.275413,-0.131566 0.42162,-0.169133 0.1462,-0.03756 0.296623,-0.05604 0.450856,-0.05604 0.242609,0 0.430233,0.05684 0.563581,0.17115 0.134955,0.11268 0.202565,0.272746 0.202565,0.480136 v 0.04392 h -0.621528 c -0.403268,0 -0.707246,0.07851 -0.91129,0.235269 -0.202437,0.156767 -0.30359,0.389605 -0.30359,0.698241 0,0.264539 0.08124,0.476223 0.243507,0.634619 0.16388,0.156767 0.384001,0.234768 0.660342,0.234768 0.218503,0 0.403518,-0.03926 0.554538,-0.11763 0.151027,-0.08001 0.276805,-0.203 0.378021,-0.369566 v 0.41652 h 0.443422 v -1.565606 c 0,-0.418043 -0.09644,-0.729773 -0.289235,-0.935527 -0.192792,-0.205756 -0.484196,-0.308477 -0.874605,-0.308478 z m 7.613074,0 c -0.387196,0 -0.691185,0.127439 -0.911295,0.382187 -0.218503,0.254745 -0.327533,0.607506 -0.327509,1.058211 -2.9e-5,0.449043 0.109006,0.801328 0.327509,1.057704 0.22011,0.254747 0.524099,0.38219 0.911295,0.38219 0.385596,0 0.688523,-0.12744 0.908633,-0.38219 0.22012,-0.256376 0.33018,-0.608661 0.33018,-1.057704 0,-0.450705 -0.11006,-0.803466 -0.33018,-1.058211 -0.22011,-0.254742 -0.523037,-0.382187 -0.908633,-0.382187 z m 6.576833,0 c -0.1944,0 -0.36264,0.03965 -0.50402,0.119655 -0.14139,0.08001 -0.26244,0.204398 -0.36368,0.372593 v -0.426111 h -0.44607 v 2.743473 h 0.44607 v -1.550461 c 0,-0.266175 0.0665,-0.476965 0.19992,-0.632099 0.13335,-0.156763 0.31311,-0.23527 0.53965,-0.23527 0.19119,0 0.3327,0.06346 0.42428,0.19084 0.0916,0.125745 0.13769,0.321111 0.13769,0.585651 v 1.641339 h 0.44555 v -1.550461 c 0,-0.267809 0.0665,-0.479488 0.19991,-0.634624 0.13336,-0.155117 0.31522,-0.232745 0.54497,-0.232745 0.18798,0 0.32738,0.06346 0.41896,0.19084 0.0915,0.127365 0.13771,0.322743 0.13771,0.585651 v 1.641339 h 0.44554 v -1.655979 c 0,-0.369052 -0.0759,-0.653001 -0.22862,-0.852222 -0.15263,-0.200856 -0.3696,-0.301409 -0.65077,-0.301409 -0.20886,0 -0.3902,0.04805 -0.54443,0.144389 -0.15425,0.09635 -0.28685,0.245838 -0.3977,0.448328 -0.0659,-0.192693 -0.16808,-0.33951 -0.30625,-0.440754 -0.13656,-0.101247 -0.30269,-0.151966 -0.49871,-0.151966 z m 4.55699,0 c -0.38719,0 -0.69065,0.127439 -0.91076,0.382187 -0.21849,0.254745 -0.32804,0.607506 -0.32804,1.058211 0,0.449043 0.10955,0.801328 0.32804,1.057704 0.22011,0.254747 0.52357,0.38219 0.91076,0.38219 0.3856,0 0.68853,-0.12744 0.90864,-0.38219 0.22011,-0.256376 0.33018,-0.608661 0.33018,-1.057704 0,-0.450705 -0.11008,-0.803466 -0.33018,-1.058211 -0.22011,-0.254742 -0.52304,-0.382187 -0.90864,-0.382187 z m 3.27514,0 c -0.18798,0 -0.35412,0.04053 -0.49871,0.122175 -0.143,0.08164 -0.26774,0.205139 -0.37377,0.370069 v -0.426111 h -0.44555 v 2.743472 h 0.44555 V 27.07893 c 0,-0.267808 0.0692,-0.479487 0.20735,-0.634623 0.13817,-0.155118 0.32684,-0.232746 0.56623,-0.232746 0.19923,0 0.3486,0.06436 0.4482,0.193365 0.0996,0.129001 0.14942,0.323486 0.14942,0.583127 v 1.641338 h 0.4434 v -1.655978 c 0,-0.378848 -0.0791,-0.665474 -0.23818,-0.859797 -0.15906,-0.195956 -0.39386,-0.293834 -0.70394,-0.293834 z m -20.262723,0.06613 1.091537,2.699045 -0.07923,0.205986 c -0.08193,0.218821 -0.156889,0.357732 -0.224367,0.416519 -0.06749,0.05879 -0.162342,0.08786 -0.284449,0.08786 h -0.259993 v 0.37714 h 0.354101 c 0.199219,0 0.358023,-0.04983 0.476911,-0.149446 0.118894,-0.09962 0.241007,-0.312568 0.366328,-0.639168 l 1.185636,-2.997925 h -0.469469 l -0.843773,2.145704 -0.843239,-2.145704 z m -4.820178,0.311001 c 0.23457,0 0.419584,0.09483 0.554539,0.284243 0.13496,0.187792 0.202571,0.44752 0.202571,0.779017 0,0.331467 -0.06761,0.591612 -0.202571,0.781036 -0.134955,0.187794 -0.319969,0.281717 -0.554539,0.281717 -0.234568,0 -0.419049,-0.09393 -0.554003,-0.281717 -0.133348,-0.189419 -0.19991,-0.449564 -0.19991,-0.781031 0,-0.331496 0.06656,-0.591225 0.19991,-0.779017 0.134954,-0.189416 0.319435,-0.284243 0.554003,-0.284243 z m 10.651603,0.0051 c 0.0074,-1.84e-4 0.01491,0 0.02233,0 0.236182,0 0.423279,0.09519 0.561448,0.286263 0.13817,0.189425 0.20732,0.446984 0.20736,0.77195 0,0.326568 -0.0692,0.584539 -0.20736,0.773967 -0.138169,0.189424 -0.325266,0.283737 -0.561448,0.283737 -0.239389,0 -0.428063,-0.09342 -0.566236,-0.281214 -0.13656,-0.189425 -0.205227,-0.44829 -0.205227,-0.77649 0,-0.328232 0.06918,-0.586682 0.207356,-0.774474 0.133852,-0.183503 0.314446,-0.278005 0.541777,-0.283739 z m 11.133828,0 c 0.007,-1.84e-4 0.0149,0 0.0223,0 0.23617,0 0.42329,0.09519 0.56147,0.286263 0.13816,0.189425 0.20735,0.446984 0.20735,0.77195 0,0.326568 -0.0692,0.584539 -0.20735,0.773967 -0.13818,0.189424 -0.3253,0.283737 -0.56147,0.283737 -0.23939,0 -0.42806,-0.09342 -0.56623,-0.281214 -0.13656,-0.189425 -0.20469,-0.44829 -0.20469,-0.77649 0,-0.328232 0.0686,-0.586682 0.20682,-0.774474 0.13385,-0.183503 0.31444,-0.278005 0.54177,-0.283739 z M 92.194771,27.25013 h 0.44076 v 0.100467 c 0,0.293937 -0.07441,0.529836 -0.223834,0.707832 -0.147812,0.17636 -0.344873,0.264551 -0.59069,0.264551 -0.178338,0 -0.319846,-0.04626 -0.424281,-0.13935 -0.102822,-0.09471 -0.154181,-0.222152 -0.154181,-0.382189 0,-0.200828 0.06915,-0.342827 0.20735,-0.426109 0.138168,-0.08328 0.386599,-0.125205 0.744876,-0.125205 z'; - -function getRandomHex(bytes = 32) { - const array = new Uint8Array(bytes); - window.crypto.getRandomValues(array); - return Array.from(array, b => b.toString(16).padStart(2, '0')).join(''); -} - -const isNumber = (value: string): boolean => { - return !isNaN(value as any); -} - -const getObject = (value: any): any => { - if (!value) { - return undefined; - } - if (Object.keys(value).length === 0) { - return undefined; - } - if (Object.values(value).filter(Boolean).length === 0) { - return undefined; - } - return value; -} - -const CONFIG_API_FIELDS = [ - {name: 'host', label: 'LABEL.HOST', fieldType: FormFieldType.TEXT, validator: undefined}, - {name: 'port', label: 'LABEL.PORT', fieldType: FormFieldType.NUMBER, validator: isNumber}, - {name: 'web_root', label: 'LABEL.WEB_ROOT', fieldType: FormFieldType.TEXT, validator: undefined}, - -]; - -const CONFIG_WEB_UI_FIELDS = [ - {name: 'enabled', label: 'LABEL.WEB_UI', fieldType: FormFieldType.CHECK}, - {name: 'path', label: 'LABEL.WEB_UI_PATH', fieldType: FormFieldType.TEXT, hint: 'HINT.CONFIG.WEB_UI.PATH'}, - {name: 'user_ui_enabled', label: 'LABEL.USER_UI', fieldType: FormFieldType.CHECK}, - {name: 'player_server', label: 'LABEL.PLAYER_SERVER', fieldType: FormFieldType.SINGLE_SELECT}, -]; - -const CONFIG_WEB_AUTH_FIELDS = [ - {name: 'enabled', label: 'LABEL.WEB_AUTH', fieldType: FormFieldType.CHECK}, - {name: 'issuer', label: 'LABEL.ISSUER', fieldType: FormFieldType.TEXT}, - { - name: 'secret', - label: 'LABEL.SECRET', - fieldType: FormFieldType.TEXT, - action: {icon: 'Refresh', handler: getRandomHex} - }, - {name: 'userfile', label: 'LABEL.USERFILE', fieldType: FormFieldType.TEXT}, -]; - -const CONFIG_FIELDS = [ - {name: 'threads', label: 'LABEL.THREADS', fieldType: FormFieldType.NUMBER, validator: isNumber}, - {name: 'working_dir', label: 'LABEL.WORKING_DIR', fieldType: FormFieldType.TEXT, validator: undefined}, - {name: 'backup_dir', label: 'LABEL.BACKUP_DIR', fieldType: FormFieldType.TEXT, validator: undefined}, - {name: 'update_on_boot', label: 'LABEL.UPDATE_ON_BOOT', fieldType: FormFieldType.CHECK, validator: undefined}, -]; - -const CONFIG_SCHEDULES_FIELDS = [ - {name: 'schedules', label: 'LABEL.SCHEDULES', fieldType: FormFieldType.SCHEDULE}, -]; - -const CONFIG_LOG_FIELDS = [ - {name: 'sanitize_sensitive_info', label: 'LABEL.SANITIZE_SENSITIVE_INFO', fieldType: FormFieldType.CHECK}, - {name: 'log_active_user', label: 'LABEL.ACTIVE_USER', hint: 'HINT.CONFIG.LOG.ACTIVE_USER', fieldType: FormFieldType.CHECK}, - {name: 'log_level', label: 'LABEL.LOG_LEVEL', fieldType: FormFieldType.SINGLE_SELECT, - options:[ - {label: 'info', value: 'info'}, - {label: 'debug', value: 'debug'}, - {label: 'warn', value: 'warn'}, - {label: 'error', value: 'error'}, - {label: 'trace', value: 'trace'}, - ]}, -] - -const CONFIG_REVERSE_PROXY_FIELDS = [ - { - name: 'resource_rewrite_disabled', - label: 'LABEL.RESOURCE_REWRITE_DISABLE', - hint: 'HINT.CONFIG.PROXY.RESOURCE_REWRITE_DISABLE', - fieldType: FormFieldType.CHECK - }, - { - name: 'disable_referer_header', - label: 'LABEL.DISABLE_REFERER_HEADER', - hint: 'HINT.CONFIG.PROXY.DISABLE_REFERER_HEADER', - fieldType: FormFieldType.CHECK - }, -]; - -const CONFIG_REVERSE_PROXY_STREAM_FIELDS = [ - {name: 'retry', label: 'LABEL.RECONNECT', fieldType: FormFieldType.CHECK}, - {name: 'throttle', label: 'LABEL.THROTTLE', hint: 'HINT.CONFIG.PROXY.THROTTLE', fieldType: FormFieldType.TEXT}, - {name: 'grace_period_millis', label: 'LABEL.GRACE_PERIOD', fieldType: FormFieldType.NUMBER}, - {name: 'grace_period_timeout_secs', label: 'LABEL.GRACE_PERIOD_TIMEOUT', fieldType: FormFieldType.NUMBER}, - -]; - -const CONFIG_REVERSE_PROXY_STREAM_BUFFER_FIELDS = [ - {name: 'enabled', label: 'LABEL.BUFFER_ENABLED', fieldType: FormFieldType.CHECK}, - {name: 'size', label: 'LABEL.BUFFER_SIZE', fieldType: FormFieldType.NUMBER}, -]; - -const CONFIG_REVERSE_PROXY_CACHE_FIELDS = [ - {name: 'enabled', label: 'LABEL.ENABLED', fieldType: FormFieldType.CHECK}, - {name: 'size', label: 'LABEL.SIZE', fieldType: FormFieldType.TEXT}, - {name: 'dir', label: 'LABEL.CACHE_DIR', fieldType: FormFieldType.TEXT}, -]; - -const CONFIG_REVERSE_PROXY_RATE_LIMIT_FIELDS = [ - {name: 'enabled', label: 'LABEL.ENABLED', fieldType: FormFieldType.CHECK}, - {name: 'period_millis', label: 'LABEL.PERIOD_MILLIS', fieldType: FormFieldType.NUMBER}, - {name: 'burst_size', label: 'LABEL.BURST_SIZE', fieldType: FormFieldType.NUMBER}, -]; - -const CONFIG_MESSAGING_FIELDS = [ - { - name: 'notify_on', label: 'LABEL.NOTIFY_ON', fieldType: FormFieldType.MULTI_SELECT, options: - Object.keys(MessageKind).map((key) => ({value: (MessageKind as any)[key], label: key})) - }, -]; - -const CONFIG_TELEGRAM_FIELDS = [ - {name: 'bot_token', label: 'LABEL.BOT_TOKEN', fieldType: FormFieldType.TEXT}, - {name: 'chat_ids', label: 'LABEL.CHAT_IDS', fieldType: FormFieldType.TAGS}, -]; - -const CONFIG_REST_FIELDS = [ - {name: 'url', label: 'LABEL.URL', fieldType: FormFieldType.TEXT}, -]; -const CONFIG_PUSHOVER_FIELDS = [ - {name: 'url', label: 'LABEL.URL', fieldType: FormFieldType.TEXT}, - {name: 'user', label: 'LABEL.USER', fieldType: FormFieldType.TEXT}, - {name: 'token', label: 'LABEL.TOKEN', fieldType: FormFieldType.TEXT}, -]; - -const CONFIG_VIDEO_FIELDS = [ - {name: 'web_search', label: 'LABEL.WEB_SEARCH_URL', fieldType: FormFieldType.TEXT}, - {name: 'extensions', label: 'LABEL.EXTENSIONS', fieldType: FormFieldType.TEXT}, -]; - -const CONFIG_VIDEO_DOWNLOAD_FIELDS = [ - {name: 'headers', label: 'LABEL.HEADERS', fieldType: FormFieldType.MAP}, - {name: 'directory', label: 'LABEL.DIRECTORY', fieldType: FormFieldType.TEXT}, - {name: 'organize_into_directories', label: 'LABEL.ORGANIZE_INTO_DIRECTORIES', fieldType: FormFieldType.CHECK}, - {name: 'episode_pattern', label: 'LABEL.EPISODE_PATTERN', fieldType: FormFieldType.TEXT}, -]; - -const TABS = [ - {label: 'LABEL.API', key: 'api'}, - {label: 'LABEL.MAIN', key: 'main'}, - {label: 'LABEL.WEB_UI', key: 'web_ui'}, - {label: 'LABEL.PROXY', key: 'proxy'}, - {label: 'LABEL.SCHEDULES', key: 'schedules'}, - {label: 'LABEL.REVERSE_PROXY', key: 'reverseProxy'}, - {label: 'LABEL.MESSAGING', key: 'messaging'}, - {label: 'LABEL.VIDEO', key: 'video'} -]; - -const CONFIG_PROXY_FIELDS = [ - {name: 'url', label: 'LABEL.URL', fieldType: FormFieldType.TEXT}, - {name: 'username', label: 'LABEL.USERNAME', fieldType: FormFieldType.TEXT}, - {name: 'password', label: 'LABEL.PASSWORD', fieldType: FormFieldType.TEXT}, -]; - -const CONFIG_IP_CHECK_FIELDS = [ - {name: 'url', label: 'LABEL.URL', fieldType: FormFieldType.TEXT}, - {name: 'url_ipv4', label: 'LABEL.URL_IPV4', fieldType: FormFieldType.TEXT}, - {name: 'pattern_ipv4', label: 'LABEL.PATTERN_IPV4', fieldType: FormFieldType.TEXT}, - {name: 'url_ipv6', label: 'LABEL.URL_IPV6', fieldType: FormFieldType.TEXT}, - {name: 'pattern_ipv6', label: 'LABEL.PATTERN_IPV6', fieldType: FormFieldType.TEXT}, -]; - - -// const translateLabels = (config: any, translateFunc: any) => config.map((c: any) => ({ -// ...c, -// label: translateFunc(c.label) -// })); - -const translateLabels = (config: any, translateFunc: any) => config; - -interface MainConfigViewProps { - config: ServerConfig; -} - -export default function MainConfigView(props: MainConfigViewProps) { - const {config} = props; - const services = useServices(); - const translate = useTranslator(); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const [activeTab, setActiveTab] = useState('api'); - const apiConfig = useMemo(() => config?.api || {} as any, [config]); - const mainConfig = useMemo(() => config, [config]); - const webUiConfig = useMemo(() => config?.web_ui || {} as any, [config]); - const webAuthConfig = useMemo(() => config?.web_ui?.auth || {} as any, [config]); - const messagingConfig = useMemo(() => config?.messaging || {} as any, [config]); - const telegramConfig = useMemo(() => config?.messaging?.telegram || {} as any, [config]); - const restConfig = useMemo(() => config?.messaging?.rest || {} as any, [config]); - const pushoverConfig = useMemo(() => config?.messaging?.pushover || {} as any, [config]); - const videoConfig = useMemo(() => config?.video || {} as any, [config]); - const videoDownloadConfig = useMemo(() => config?.video?.download || {} as any, [config]); - const reverseProxyConfig = useMemo(() => config?.reverse_proxy || {} as any, [config]); - const reverseProxyStreamConfig = useMemo(() => config?.reverse_proxy?.stream || {} as any, [config]); - const reverseProxyStreamBufferConfig = useMemo(() => config?.reverse_proxy?.stream?.buffer || {} as any, [config]); - const reverseProxyCacheConfig = useMemo(() => config?.reverse_proxy?.cache || {} as any, [config]); - const reverseProxyRateLimitConfig = useMemo(() => config?.reverse_proxy?.rate_limit || {} as any, [config]); - const logConfig = useMemo(() => config?.log || {} as any, [config]); - const proxyConfig = useMemo(() => config?.proxy || {} as any, [config]); - const ipCheckConfig = useMemo(() => config?.ipcheck || {} as any, [config]); - const configs = useMemo(() => { - CONFIG_WEB_UI_FIELDS.forEach(def => { - const serverOptions = config?.api_proxy?.server?.map(serverInfo => ({ - value: serverInfo.name, - label: serverInfo.name - })); - if (def.name === "player_server") { - (def as any).options = serverOptions; - } - }); - const translations: any = { - api_fields: translateLabels(CONFIG_API_FIELDS, translate), - fields: translateLabels(CONFIG_FIELDS, translate), - schedules_fields: translateLabels(CONFIG_SCHEDULES_FIELDS, translate), - log_fields: translateLabels(CONFIG_LOG_FIELDS, translate), - web_ui_config_fields: translateLabels(CONFIG_WEB_UI_FIELDS, translate), - web_auth_config_fields: translateLabels(CONFIG_WEB_AUTH_FIELDS, translate), - reverse_proxy_fields: translateLabels(CONFIG_REVERSE_PROXY_FIELDS, translate), - reverse_proxy_stream_fields: translateLabels(CONFIG_REVERSE_PROXY_STREAM_FIELDS, translate), - reverse_proxy_stream_buffer_fields: translateLabels(CONFIG_REVERSE_PROXY_STREAM_BUFFER_FIELDS, translate), - reverse_proxy_cache_fields: translateLabels(CONFIG_REVERSE_PROXY_CACHE_FIELDS, translate), - reverse_proxy_rate_limit_fields: translateLabels(CONFIG_REVERSE_PROXY_RATE_LIMIT_FIELDS, translate), - messaging_fields: translateLabels(CONFIG_MESSAGING_FIELDS, translate), - telegram_fields: translateLabels(CONFIG_TELEGRAM_FIELDS, translate), - rest_fields: translateLabels(CONFIG_REST_FIELDS, translate), - pushover_fields: translateLabels(CONFIG_PUSHOVER_FIELDS, translate), - video_fields: translateLabels(CONFIG_VIDEO_FIELDS, translate), - video_download_fields: translateLabels(CONFIG_VIDEO_DOWNLOAD_FIELDS, translate), - proxy_config_fields: translateLabels(CONFIG_PROXY_FIELDS, translate), - ipcheck_config_fields: translateLabels(CONFIG_IP_CHECK_FIELDS, translate), - tabs: translateLabels(TABS, translate), - } - return translations; - }, [translate, config?.api_proxy?.server]); - - const scheduleImage = useMemo(() => , []); - - const handleSave = useCallback(() => { - if (mainConfig && apiConfig) { - let telegramConfigAvailable = telegramConfig?.bot_token?.trim().length && telegramConfig?.chat_ids?.length; - let restConfigAvailable = restConfig?.url?.trim().length; - let pushoverConfigAvailable = pushoverConfig?.user?.trim().length && pushoverConfig?.token?.length; - const cfgMessaging: MessagingConfig = { - notify_on: messagingConfig.notify_on, - telegram: telegramConfigAvailable ? getObject(telegramConfig) : undefined, - rest: restConfigAvailable ? getObject(restConfig) : undefined, - pushover: pushoverConfigAvailable ? getObject(pushoverConfig) : undefined, - }; - - - let reverseProxyStreamConfigAvailable = getObject(reverseProxyStreamConfig) != undefined; - let cfgReverseProxyStreamBuffer: StreamConfig = reverseProxyStreamConfigAvailable ? ({ - retry: reverseProxyStreamConfig.retry, - throttle: reverseProxyStreamConfig.throttle?.trim().length ? reverseProxyStreamConfig.throttle : undefined, - grace_period_millis: reverseProxyStreamConfig.grace_period_millis, - grace_period_timeout_secs: reverseProxyStreamConfig.grace_period_timeout_secs, - buffer: { - enabled: reverseProxyStreamBufferConfig.enabled, - size: reverseProxyStreamBufferConfig.size ?? undefined, - } - }): undefined; - - const reverseProxyConfigAvailable = getObject(reverseProxyConfig) || - getObject(cfgReverseProxyStreamBuffer) || - getObject(reverseProxyCacheConfig) || - getObject(reverseProxyRateLimitConfig); - const cfgReverseProxy: ReverseProxyConfig = reverseProxyConfigAvailable ? { - resource_rewrite_disabled: reverseProxyConfig.resource_rewrite_disabled, - disable_referer_header: reverseProxyConfig.disable_referer_header, - stream: getObject(cfgReverseProxyStreamBuffer), - cache: getObject(reverseProxyCacheConfig), - rate_limit: getObject(reverseProxyRateLimitConfig), - } : undefined; - - const cfgWebUiConfig: WebUiConfig = { - enabled: webUiConfig.enabled ?? false, - user_ui_enabled: webUiConfig.user_ui_enabled ?? false, - path: webUiConfig.path, - auth: webAuthConfig, - } - - const cfgProxyConfig: ProxyConfig = { - url: proxyConfig.url, - username: proxyConfig.username, - password: proxyConfig.password, - } - - const cfgIpCheckConfig: IpCheckConfig = { - url: ipCheckConfig.url, - url_ipv4: ipCheckConfig.url_ipv4, - url_ipv6: ipCheckConfig.url_ipv6, - pattern_ipv4: ipCheckConfig.pattern_ipv4, - pattern_ipv6: ipCheckConfig.pattern_ipv6, - } - - const cfgVideo = getObject(videoConfig) && getObject(videoDownloadConfig) ? { - ...videoConfig, - download: getObject(videoDownloadConfig) - }: undefined; - - const cfgLog = { - ...logConfig - }; - - const cfgMain: ServerMainConfig = { - api: apiConfig, - working_dir: mainConfig.working_dir, - backup_dir: mainConfig.backup_dir, - schedules: mainConfig.schedules?.filter(s => s.schedule?.trim().length), - threads: mainConfig.threads, - messaging: getObject(cfgMessaging), - reverse_proxy: getObject(cfgReverseProxy), - video: getObject(cfgVideo), - log: getObject(cfgLog), - update_on_boot: mainConfig.update_on_boot, - web_ui: getObject(cfgWebUiConfig), - proxy: getObject(cfgProxyConfig), - ipcheck: getObject(cfgIpCheckConfig), - }; - - services.config().saveMainConfig(cfgMain).subscribe({ - next: () => enqueueSnackbar(translate('MESSAGES.SAVE.MAIN_CONFIG.SUCCESS'), {variant: 'success'}), - error: (err: any) => enqueueSnackbar(translate('MESSAGES.SAVE.MAIN_CONFIG.FAIL'), {variant: 'error'}) - }); - } - }, [mainConfig, apiConfig, videoConfig, messagingConfig, telegramConfig, restConfig, pushoverConfig, - videoDownloadConfig, reverseProxyConfig, enqueueSnackbar, services, logConfig, - reverseProxyRateLimitConfig, reverseProxyCacheConfig, reverseProxyStreamConfig, reverseProxyStreamBufferConfig, - webUiConfig, webAuthConfig, proxyConfig, ipCheckConfig, - translate]); - - const handleTabChange = useCallback((tab: string) => { - setActiveTab(tab); - }, []); - - - return
-
- -
- -
-
- -
- {translate('INFO.WEB_ROOT')} -
-
-
- - - -
- {translate('INFO.THREADS')} -
-
-
- - - -
-
- - - - -
-
- -
- {translate('INFO.SCHEDULE_EXAMPLE')}: - {scheduleImage} -
-
-
- - - - - - - - -
-
- - - - - - - -
-
- - -
- {translate('LABEL.EXAMPLES')}: - {translate('LABEL.WEB_SEARCH_URL')}:
{'https://www.imdb.com/search/title/?title={}'}
- {translate('LABEL.EPISODE_PATTERN')}:
{'.*(?P[Ss]\\d{1,2}(.*?)[Ee]\\d{1,2}).*'}
-
- -
-
- -
- {getIconByName('Warn')} - {translate('INFO.RESTART_TO_APPLY_CHANGES')} -
-
-} \ No newline at end of file diff --git a/frontend/src/component/map-editor/map-editor.scss b/frontend/src/component/map-editor/map-editor.scss deleted file mode 100644 index bfa3cdfeb..000000000 --- a/frontend/src/component/map-editor/map-editor.scss +++ /dev/null @@ -1,72 +0,0 @@ -@use '../../scss/common'; - -.map-editor { - @include common.container_vert(); - flex: 1 1 auto; - overflow: hidden; - border: 1px solid var(--border-color); - border-radius: 8px; - - &__toolbar { - flex: 0 0 auto; - span { - @include common.iconButton(); - } - } - - &__table { - @include common.container_vert_no_padding(); - } - - &__row { - display: flex; - flex-flow: row nowrap; - gap: 20px; - flex: 1 1 auto; - - } - - &__col { - display: flex; - align-items: center; - } - - &__col-key, &__col-value { - user-select: none; - box-sizing: border-box; - min-width: 12rem; - border: 1px solid var(--border-color); - padding: 0 4px; - height: 2rem; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &__col-value { - flex: 1 1 auto; - } - - &__input { - display: flex; - flex-flow: row wrap; - gap: 20px; - align-items: center; - - &-key, &-value { - box-sizing: border-box; - min-width: 12rem; - display: flex; - gap: 10px; - input { - padding: 4px 0; - } - } - - button { - @include common.textButton(); - } - } - - -} \ No newline at end of file diff --git a/frontend/src/component/map-editor/map-editor.tsx b/frontend/src/component/map-editor/map-editor.tsx deleted file mode 100644 index e823dfb82..000000000 --- a/frontend/src/component/map-editor/map-editor.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import './map-editor.scss'; - -import React, {useCallback, useEffect, useMemo, useRef, useState} from "react"; -import {genUuid} from "../../utils/uuid"; -import {getIconByName} from "../../icons/icons"; - -interface KeyValue { - key: string; - value: string; -} - -const mapToList = (data: Record) => data ? Object.keys(data).map(key => ({key, value: data[key]})) : []; -const listToMap = (data: KeyValue[]) => data?.reduce((acc: any, curVal) => {acc[curVal.key] = curVal.value; return acc;}, {}); -const containsKey = (key: string, data: KeyValue[]) => data?.find((keyValue: KeyValue) => keyValue.key === key); - -interface MapEditorProps { - name: string; - values: Record; - onChange: (name: string, values: Record) => void; -} - -export default function MapEditor(props: MapEditorProps) { - const {name, values, onChange} = props; - const uuid = useMemo(() => genUuid(), []); - const keyRef = useRef(undefined); - const valRef = useRef(undefined); - const [data, setData] = useState([]); - - useEffect(() => { - if (values) { - setData(mapToList(values)); - } - }, [values]); - - const handleHeaderRemove = useCallback((event: any) => { - const key = event.target.dataset.key; - if (key) { - setData(data => { - const newData = data?.filter((keyValue: KeyValue) => keyValue.key !== key); - onChange(name, listToMap(newData)); - return newData; - }); - } - - }, [name, onChange]); - - const handleAddKeyValue = useCallback((event: any) => { - let key = keyRef.current?.value?.trim(); - let value = valRef.current?.value?.trim(); - if (key.length> 0 && value.length > 0) { - setData(data => { - let result = data; - if (!containsKey(key, data)) { - data.push({key, value}); - result = [...data]; - } - onChange(name, listToMap(result)); - return result; - }); - } - }, [name, onChange]); - - return
-
-
-
-
-
-
- {data?.map((keyValue: KeyValue, idx) => -
-
- - {getIconByName('Delete')} - -
-
{keyValue.key}
-
{keyValue.value}
-
- )} -
-
-} \ No newline at end of file diff --git a/frontend/src/component/panel/panel.scss b/frontend/src/component/panel/panel.scss deleted file mode 100644 index 150e858ff..000000000 --- a/frontend/src/component/panel/panel.scss +++ /dev/null @@ -1,9 +0,0 @@ -@use '../../scss/common'; - -.panel { - @include common.container_vert_no_padding(); - overflow: hidden; -} -.hidden { - display: none; -} \ No newline at end of file diff --git a/frontend/src/component/panel/panel.tsx b/frontend/src/component/panel/panel.tsx deleted file mode 100644 index 1e87d2de6..000000000 --- a/frontend/src/component/panel/panel.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, {ReactNode} from "react"; -import './panel.scss'; - -interface PanelProps { - value: any; - active: any; - children: ReactNode; -} - -export default function Panel(props: PanelProps) { - const {value, active, children} = props; - return
{children}
-} \ No newline at end of file diff --git a/frontend/src/component/playlist-browser/playlist-browser.scss b/frontend/src/component/playlist-browser/playlist-browser.scss deleted file mode 100644 index c09ad4c45..000000000 --- a/frontend/src/component/playlist-browser/playlist-browser.scss +++ /dev/null @@ -1,30 +0,0 @@ -.playlist-browser { - &__content { - display: flex; - flex-flow: column; - flex: 1 1 auto; - box-sizing: border-box; - overflow: hidden; - max-width: 100%; - background-color: inherit; - padding: 10px; - - .playlist-viewer { - background-color: var(--card-background-color); - } - } - - &__toolbar { - display: flex; - flex-flow: row wrap; - flex: 0 0 auto; - box-sizing: border-box; - overflow: hidden; - background-color: inherit; - gap: 12px; - - .source-selector { - flex: 1 1 auto; - } - } -} \ No newline at end of file diff --git a/frontend/src/component/playlist-browser/playlist-browser.tsx b/frontend/src/component/playlist-browser/playlist-browser.tsx deleted file mode 100644 index 0de622fab..000000000 --- a/frontend/src/component/playlist-browser/playlist-browser.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React, {useCallback, useMemo, useState} from 'react'; -import './playlist-browser.scss'; -import SourceSelector from "..//source-selector/source-selector"; -import PlaylistViewer, {SearchRequest} from "../playlist-viewer/playlist-viewer"; -import {useSnackbar} from 'notistack'; -import {useServices} from "../../provider/service-provider"; -import Progress from '..//progress/progress'; -import PlaylistFilter from "../playlist-filter/playlist-filter"; -import {finalize, first, noop, Subject} from "rxjs"; -import ClipboardViewer from "../clipboard-viewer/clipboard-viewer"; -import Sidebar from "../sidebar/sidebar"; -import {PlaylistRequest} from "../../model/playlist-request"; -import ServerConfig from "../../model/server-config"; -import FileDownload from "../file-download/file-download"; -import {FileDownloadInfo} from "../../model/file-download"; -import useTranslator from "../../hook/use-translator"; -import {EmptyPlaylistCategories, PlaylistCategories, PlaylistItem} from "../../model/playlist"; -import LoadingIndicator from "../loading-indicator/loading-indicator"; - -/* eslint-disable @typescript-eslint/no-empty-interface */ -interface PlaylistBrowserProps { - config: ServerConfig -} - -export default function PlaylistBrowser(props: PlaylistBrowserProps) { - const {config} = props; - const searchChannel = useMemo>(() => new Subject(), []); - const services = useServices(); - const [progress, setProgress] = useState(false); - const [playlist, setPlaylist] = useState(EmptyPlaylistCategories); - const [playlistRequest, setPlaylistRequest] = useState(undefined); - const clipboardChannel = useMemo>(() => new Subject(), []); - const translate = useTranslator(); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const videoChannel = useMemo(() => new Subject<[PlaylistItem, PlaylistRequest]>(), []); - const handleSourceDownload = useCallback((req: PlaylistRequest) => { - setProgress(true); - services.playlist().getPlaylistCategories(req).pipe(first(), finalize(() => setProgress(false))).subscribe({ - next: (pl: PlaylistCategories) => { - enqueueSnackbar(translate("MESSAGES.DOWNLOAD.PLAYLIST.SUCCESS"), {variant: 'success'}) - setPlaylist(pl); - setPlaylistRequest(req); - }, - error: (err) => { - enqueueSnackbar(translate("MESSAGES.DOWNLOAD.PLAYLIST.FAIL"), {variant: 'error'}); - }, - }); - }, [enqueueSnackbar, services, translate]); - - const handleFilter = useCallback((filter: string, regexp: boolean): void => { - if (!regexp) { - filter = filter.toLowerCase(); - } - searchChannel.next({filter, regexp}); - }, [searchChannel]); - - const handleProgress = useCallback((value: boolean) => { - setProgress(value); - }, []); - - const handleOnPlay = useCallback((playlistItem: PlaylistItem): void => { - videoChannel.next([playlistItem, playlistRequest]); - }, [videoChannel, playlistRequest]); - - const handleOnCopy = useCallback((playlistItem: PlaylistItem): void => { - clipboardChannel.next(playlistItem.url); - }, [clipboardChannel]); - - const handleOnDownload = useCallback((playlistItem: PlaylistItem): void => { - let filename = playlistItem.title; - const parts = playlistItem.url.split('.'); - let ext = undefined; - if (parts.length > 1) { - ext = parts[parts.length - 1]; - } - - if (config.video?.extensions?.includes(ext)) { - services.file().download({ - url: playlistItem.url, - filename: filename + '.' + ext - }).pipe(first()).subscribe({ - next: (_: FileDownloadInfo) => { - }, - error: _ => enqueueSnackbar(translate("MESSAGES.DOWNLOAD.FAIL"), {variant: 'error'}), - complete: noop, - }); - } else { - enqueueSnackbar(translate("MESSAGES.INVALID_FILETYPE"), {variant: 'error'}) - } - }, [config, enqueueSnackbar, services, translate]); - - const handleOnWebSearch = useCallback((playlistItem: PlaylistItem): void => { - if (playlistItem) { - let title = playlistItem.title; - let pattern = config.video.download?.episode_pattern; - if (pattern) { - pattern = pattern.replace('?P', '?'); - const matches = title.match(pattern); - if (matches?.groups?.episode) { - const idx = title.indexOf(matches?.groups?.episode); - title = title.substring(0, idx).trim(); - } - } - const dateSuffixMatch = title.match(/(.*?).\(\d+\)/); - if (dateSuffixMatch?.length > 1) { - title = dateSuffixMatch[1]; - } - - const url = config.video.web_search.replace("{}", title); - window.open(url, "imdb"); - } - }, [config]); - - - return (<> - - - -
-
- - -
- - - - -
- ); -} diff --git a/frontend/src/component/playlist-filter/playlist-filter.scss b/frontend/src/component/playlist-filter/playlist-filter.scss deleted file mode 100755 index 301d60d70..000000000 --- a/frontend/src/component/playlist-filter/playlist-filter.scss +++ /dev/null @@ -1,30 +0,0 @@ -@use "../../scss/common"; - -.playlist-filter { - overflow: hidden; - display: flex; - flex-flow: row nowrap; - padding: 10px 0 10px 0; - max-height: 2.5rem; - font-size: 0.5rem; - flex: 1 1 auto; - max-width: 400px; - background-color: inherit; - - .input-field { - flex: 1 1 auto; - } - - &__option-active { - @include common.activeOptionButton(); - } - - &__error-message { - display: flex; - flex-flow: row; - color: var(--attention-color); - white-space: nowrap; - align-items: center; - margin-left: 8px; - } -} \ No newline at end of file diff --git a/frontend/src/component/playlist-filter/playlist-filter.tsx b/frontend/src/component/playlist-filter/playlist-filter.tsx deleted file mode 100755 index d44028874..000000000 --- a/frontend/src/component/playlist-filter/playlist-filter.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, {KeyboardEvent, useCallback, useEffect, useRef, useState} from "react"; -import './playlist-filter.scss'; -import {getIconByName} from "../../icons/icons"; -import InputField from "../input-field/input-field"; -import useTranslator from "../../hook/use-translator"; - -function isValidRegExp(pattern: string): boolean { - try { - new RegExp(pattern); - return true; - } catch (e) { - return false; - } -} - -interface PlaylistFilterProps { - options?: { filter: string, regexp: boolean } - onFilter: (filter: string, regexp: boolean) => void; -} - -export default function PlaylistFilter(props: PlaylistFilterProps) { - const {onFilter, options} = props; - const translate = useTranslator(); - const textField = useRef(undefined); - const [useRegexp, setUseRegexp] = useState(false); - const [errorMsg, setErrorMsg] = useState(undefined); - - useEffect(()=> { - setUseRegexp(options?.regexp ?? false); - if (textField.current) { - textField.current.value = options?.filter ?? ''; - } - }, [options?.regexp, options?.filter]); - - const handleSearch = useCallback(() => { - const value = textField.current.value; - setErrorMsg(undefined); - if (useRegexp && value?.trim() !== '') { - if (isValidRegExp(value)) { - onFilter(value, useRegexp); - } else { - setErrorMsg(translate('MESSAGES.INVALID_REGEXP')); - } - } else { - onFilter(value, useRegexp); - } - }, [onFilter, useRegexp, translate]); - - const handleClear = useCallback(() => { - textField.current.value = ''; - handleSearch(); - }, [handleSearch]); - - const handleKeyPress = useCallback((event: KeyboardEvent) => { - if (event.key === 'Enter') { - handleSearch(); - } - }, [handleSearch]); - - const handleRegexp = useCallback(() => { - setUseRegexp(value => !value); - }, []); - - return
- - - - - - - {errorMsg &&
{errorMsg}
} -
-} \ No newline at end of file diff --git a/frontend/src/component/playlist-gallery/playlist-gallery.scss b/frontend/src/component/playlist-gallery/playlist-gallery.scss deleted file mode 100644 index 64806f689..000000000 --- a/frontend/src/component/playlist-gallery/playlist-gallery.scss +++ /dev/null @@ -1,222 +0,0 @@ -@use '../../scss/common'; - -$card-width: 180px; -$card-height: 200px; - -$category-size: 96px; - -.playlist-gallery { - display: flex; - flex-flow: column; - overflow: hidden; - flex: 1 1 auto; - gap: 8px; - - &__breadcrumbs { - flex-flow: row; - align-items: center; - //height: 2em; - //min-height: 2em; - display: flex; - //overflow: hidden; - gap: 4px; - border-radius: 5px; - background-color: var(--breadcrumbs-background-color); - - button { - @include common.iconButton(); - } - - span { - cursor: pointer; - background-color: var(--breadcrumb-background-color); - color: var(--breadcrumb-color); - line-height: 1.8em; - text-align: center; - padding: 0 4px 0 20px; - position: relative; - - &:after { - background: var(--breadcrumb-background-color); - bottom: 0; - clip-path: polygon(50% 50%, -50% -50%, 0 100%); - content: ''; - left: 100%; - position: absolute; - top: 0; - width: 2em; - z-index: 1; - } - - &:before { - background: var(--breadcrumbs-background-color); - bottom: 0; - clip-path: polygon(-50% -50%, 50% 50%, 0 100%); - content: ''; - left: 0; - position: absolute; - top: 0; - width: 2em; - } - - &:last-child { - border-right: none; - - &:after { - background: var(--breadcrumb-active-background-color); - } - - background: var(--breadcrumb-active-background-color); - color: var(--breadcrumb-active-color); - } - - &:not(:last-child) { - &:hover { - &:after { - background: var(--text-button-hover-background-color); - } - - background: var(--text-button-hover-background-color); - color: var(--text-button-hover-color); - } - } - } - } - - &__categories { - display: flex; - flex-flow: row; - justify-content: center; - align-items: center; - flex: 1 1 auto; - gap: 12px; - } - - &__category { - display: flex; - flex-flow: row; - justify-content: center; - align-items: center; - min-height: $category-size; - min-width: $category-size; - height: $category-size; - width: $category-size; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - padding: 10px; - cursor: pointer; - - svg { - width: 100%; - height: 100%; - } - - &:hover { - background-color: var(--icon-button-hover-background-color); - color: var(--icon-button-hover-color); - } - } - - &__content { - display: flex; - flex-flow: column; - width: 100%; - border-radius: var(--border-radius); - overflow: hidden; - } - - .channel-Live { - } - - .channel-Video { - } - - .channel-Series { - } - - $grid-cell-width: 300px; - - &__channels, - &__groups { - display: grid; - grid-gap: 10px; - grid-template-columns: repeat(auto-fill, minmax($grid-cell-width, 1fr)); - overflow-x: hidden; - overflow-y: auto; - gap: 8px; - padding: 4px; - background-color: var(--background-color); - width: 100%; - box-sizing: border-box; - border-radius: var(--border-radius); - - &-count { - margin-left: 10px; - } - - &-channel, - &-group { - border: 1px solid var(--border-color); - padding: 12px 8px; - border-radius: var(--border-radius); - background-color: var(--card-background-color); - color: var(--card-color); - box-sizing: border-box; - gap: 8px; - cursor: pointer; - overflow: hidden; - text-overflow: ellipsis; - min-height: 3em; - - &:hover { - color: var(--icon-button-hover-color); - background-color: var(--icon-button-hover-background-color); - } - } - } - - &__channels { - &-channel { - position: relative; - height: 240px; - width: $grid-cell-width; - - &-logo { - display: flex; - justify-content: center; - align-content: center; - position: absolute; - left: 12px; - top: 12px; - bottom: 12px; - right: 12px; - img { - fill: white; - aspect-ratio: 1 / 1; - } - } - - &-name { - position: absolute; - left: 0; - bottom: 0; - width: $grid-cell-width; - height: 4em; - padding-top: 10px; - text-align: center; - background-color: #00000099; - font-weight: bold; - font-size: 1.2em; - } - } - } - - &__groups { - &-group { - display: flex; - flex-flow: row nowrap; - height: 48px; - white-space: nowrap; - } - } -} diff --git a/frontend/src/component/playlist-gallery/playlist-gallery.tsx b/frontend/src/component/playlist-gallery/playlist-gallery.tsx deleted file mode 100644 index 7df16d778..000000000 --- a/frontend/src/component/playlist-gallery/playlist-gallery.tsx +++ /dev/null @@ -1,371 +0,0 @@ -import ServerConfig from "../../model/server-config"; -import {PlaylistCategories, PlaylistCategory, PlaylistGroup, PlaylistItem, XtreamCluster} from "../../model/playlist"; -import './playlist-gallery.scss'; -import React, {ReactNode, useCallback, useEffect, useState} from "react"; -import {getIconByName} from "../../icons/icons"; -import useTranslator from "../../hook/use-translator"; - -const getLogo = (item: PlaylistItem): string | undefined => { - if (item.logo_small && item.logo_small.length > 0) { - return item.logo_small; - } - if (item.logo && item.logo.length > 0) { - return item.logo; - } - return undefined; -} - -interface PlaylistGalleryProps { - serverConfig: ServerConfig; - data: PlaylistCategories; - onCopy: (playlistItem: PlaylistItem) => void; - onPlay?: (playlistItem: PlaylistItem) => void; - onDownload?: (playlistItem: PlaylistItem) => void; - onWebSearch?: (playlistItem: PlaylistItem) => void; - -} - -export default function PlaylistGallery(props: PlaylistGalleryProps) { - - const {data, onCopy, onPlay, onWebSearch, onDownload, serverConfig} = props; - // const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const translate = useTranslator(); - const [selectedItem, setSelectedItem] = useState(); - const [selectedGroup, setSelectedGroup] = useState(); - const [selectedCategory, setSelectedCategory] = useState(); - - // useEffect(() => { - // if (category) { - // setGroup((data as any)?.[category]) - // } - // }, [data, category]); - // - // const getPlaylistItemById = useCallback((itemId: string): PlaylistItem => { - // // const id = parseInt(itemId); - // // if (data && !isNaN(id)) { - // // for (let i = 0, len = data.length; i < len; i++) { - // // const group = data[i]; - // // for (let j = 0, clen = group.channels.length; j < clen; j++) { - // // const plitem = group.channels[j]; - // // // eslint-disable-next-line eqeqeq - // // if (plitem.id == id) { - // // return plitem; - // // } - // // } - // // } - // // } - // return undefined; - // }, [data]); - // - // const handleClipboardUrl = useCallback((e: any) => { - // const item = getPlaylistItemById(e.target.dataset.item); - // if (item) { - // onCopy(item); - // copyToClipboard(item.header.url).pipe(first()).subscribe({ - // next: value => enqueueSnackbar(value ? "URL copied to clipboard" : "Copy to clipboard failed!", {variant: value ? 'success' : 'error'}), - // error: _ => enqueueSnackbar("Copy to clipboard failed!", {variant: 'error'}), - // complete: noop, - // }); - // } - // }, [enqueueSnackbar, getPlaylistItemById, onCopy]); - - // const handleWebSearch = useCallback((e: any) => { - // if (onWebSearch) { - // const item = getPlaylistItemById(e.target.dataset.item); - // if (item) { - // onWebSearch(item); - // } - // } - // }, [getPlaylistItemById, onWebSearch]); - - // const handleDownloadUrl = useCallback((e: any) => { - // if (onDownload) { - // if (!serverConfig.video.download?.directory) { - // enqueueSnackbar("Please updated the server configuration and add video.download directory and headers!", {variant: 'error'}) - // } else { - // const item = getPlaylistItemById(e.target.dataset.item); - // if (item) { - // onDownload(item); - // } - // } - // } - // }, [serverConfig, enqueueSnackbar, getPlaylistItemById, onDownload]); - - // const handlePlayUrl = useCallback((e: any) => { - // if (onPlay) { - // const item = getPlaylistItemById(e.target.dataset.item); - // if (item) { - // onPlay(item); - // } - // } - // }, [onPlay, getPlaylistItemById]); - // - // const isVideoFile = useCallback((entry: PlaylistItem): boolean => { - // if (serverConfig?.video?.extensions && entry.header.url) { - // for (const ext of serverConfig.video.extensions) { - // if (entry.header.url.endsWith(ext)) { - // return true; - // } - // } - // } - // return false; - // }, [serverConfig]); - - // const handleGroupClick = useCallback((event: any) => { - // // const key = parseInt(event.target.dataset.group); - // // const group = data.find(plg => plg.id === key); - // // setShowcaseGroup(group); - // }, [data]); - // - // const renderGroup = useCallback((group: PlaylistGroup): React.ReactNode => { - // return
- // {group.name} - //
- // }, [handleGroupClick, group]); - // - // const renderPlaylistItem = useCallback((playlistItem: PlaylistItem) => { - // return
- //
- // {playlistItem.header.name} - //
- //
- // logo - //
- //
- //
- // {getIconByName('LinkRounded')} - //
- //
- // {getIconByName('PlayArrow')} - //
- // {isVideoFile(playlistItem) && - // <> - //
- // {getIconByName('Download')} - //
- // {serverConfig.video?.web_search && - //
- // {getIconByName('WebSearch')} - //
- // } - // - // } - //
- //
- // }, [handleClipboardUrl, handleDownloadUrl, handlePlayUrl, handleWebSearch, isVideoFile, serverConfig]); - - // const renderShowcase = useCallback(() => { - // if (!group) { - // return - // } - // return group.channels?.map(renderPlaylistItem); - // }, [group, renderPlaylistItem]); - - // useEffect(() => { - // if (selectedItem) { - // - // } - // }, [selectedItem]); - - useEffect(() => { - if (selectedCategory) { - let groups = (data as any)[selectedCategory]; - if (!groups?.length) { - setSelectedItem(undefined); - setSelectedGroup(undefined); - setSelectedCategory(undefined); - } - if (selectedGroup && groups?.length) { - let group = groups.find((g: PlaylistGroup) => g.id === selectedGroup.id); - if (!group) { - setSelectedItem(undefined); - setSelectedGroup(undefined); - } - if (group && selectedItem) { - let channel = group.channels.find((c: PlaylistItem) => c.id === selectedItem.id); - if (!channel) { - setSelectedItem(undefined); - } - } - } - } - }, [data, selectedCategory, selectedGroup, selectedItem]) - - const handleCategorySelect = useCallback((evt: any) => { - let category = evt.target.dataset.category; - setSelectedCategory(category); - }, []); - - const handleGroupSelect = useCallback((evt: any) => { - let category = evt.target.dataset.category; - let groupId = evt.target.dataset.group; - let groups: PlaylistGroup[] = (data as any)[category]; - if (groups) { - // eslint-disable-next-line eqeqeq - let group = groups.find(grp => grp.id == groupId); - setSelectedGroup(group) - } - }, [data]); - - const handleChannelSelect = useCallback((evt: any) => { - let category = evt.target.dataset.category; - if (category !== PlaylistCategory.LIVE) { - let groupId = evt.target.dataset.group; - let channelId = evt.target.dataset.channel; - let groups: PlaylistGroup[] = (data as any)[category]; - if (groups) { - // eslint-disable-next-line eqeqeq - let group = groups.find(grp => grp.id == groupId); - if (group) { - // eslint-disable-next-line eqeqeq - let channel = group.channels.find(channel => channel.id == channelId); - setSelectedItem(channel) - } - } - } - }, [data]); - - const handleBack = useCallback(() => { - if (selectedItem) { - setSelectedItem(undefined); - return; - } - if (selectedGroup) { - setSelectedGroup(undefined); - return; - } - if (selectedCategory) { - setSelectedCategory(undefined); - return; - } - }, [selectedCategory, selectedGroup, selectedItem]); - - - const renderItem = useCallback((item: PlaylistItem): ReactNode => { - if (item.xtream_cluster === XtreamCluster.Video) { - return
onPlay?.(item)} className="playlist-gallery"> - {JSON.stringify(item)} -
{item.title}
- {'logo'} -
; - - } - return
{JSON.stringify(item)}
; - }, [onPlay]); - - const renderItems = useCallback((category: PlaylistCategory, group: PlaylistGroup): ReactNode => { - return
- {group.channels.map(channel => ( -
-
onPlay?.(channel)}> - {getLogo(channel) && {"logo"} (e.target.onerror=null) || (e.target.src="assets/missing-logo.svg")}/>} -
-
- {channel.title} -
-
- ))} -
; - }, [handleChannelSelect, onPlay]); - - const renderGroups = useCallback((category: PlaylistCategory, groups: PlaylistGroup[]): ReactNode => { - return
- {groups.map(group => ( -
- {group.name} - {(group.channels?.length ?? 0)} -
- ))} -
; - }, [handleGroupSelect]); - - const renderCategories = useCallback((categories: PlaylistCategories) => { - const newNode = (tooltip: string, cat: string, icon: string) => { - return
{getIconByName(icon)}
; - }; - let nodes = []; - if (categories?.live?.length) { - nodes.push(newNode('LABEL.LIVE', 'live', 'Live')); - } - if (categories?.vod?.length) { - nodes.push(newNode('LABEL.VOD', 'vod', 'VOD')); - } - if (categories?.series?.length) { - nodes.push(newNode('LABEL.SERIES', 'series', 'Series')); - } - if (nodes.length) { - return
{nodes}
- } - return
{translate("MESSAGES.NO_CONTENT")}
- }, [translate, handleCategorySelect]); - - const renderSelectionContent = useCallback((): ReactNode => { - if (selectedItem) { - return renderItem(selectedItem); - } - if (selectedGroup) { - return renderItems(selectedCategory, selectedGroup); - } - if (selectedCategory) { - return renderGroups(selectedCategory, data[selectedCategory]); - } - return
{translate("MESSAGES.NO_CONTENT")}
- }, [data, selectedCategory, selectedGroup, selectedItem, - renderGroups, renderItems, renderItem, translate]) - - const renderContent = useCallback((): ReactNode => { - if (data) { - if (selectedItem || selectedGroup || selectedCategory) { - return
- {renderSelectionContent()} -
; - } - if (data) { - return renderCategories(data); - } - } - return
{translate("MESSAGES.NO_CONTENT")}
; - }, [data, selectedCategory, selectedGroup, selectedItem, renderCategories, renderSelectionContent, translate]); - - const handleBreadcrumb = useCallback((evt: any) => { - const index = evt.target.dataset.index; - // eslint-disable-next-line eqeqeq - if (index != undefined) { - switch (index) { - case '0': { - setSelectedGroup(undefined); - setSelectedItem(undefined); - break; - } - case '1': { - setSelectedItem(undefined); - break; - } - } - } - }, []); - - const renderBreadcrumbs = useCallback((): ReactNode => { - const crumbs = [selectedCategory, selectedGroup?.name, selectedItem?.title].filter(Boolean); - if (crumbs.length) { - return
- - {crumbs.map((b, index) => {b})} -
- } - return <> - }, [selectedCategory, selectedGroup, selectedItem, handleBack, handleBreadcrumb]) - - return
- {renderBreadcrumbs()} - {renderContent()} -
-} diff --git a/frontend/src/component/playlist-tree/playlist-tree.scss b/frontend/src/component/playlist-tree/playlist-tree.scss deleted file mode 100644 index 71e83d7a1..000000000 --- a/frontend/src/component/playlist-tree/playlist-tree.scss +++ /dev/null @@ -1,100 +0,0 @@ -@use '../../scss/common'; - -.playlist-tree { - - font-size: 1rem; - - .tree-expander { - margin-right: 6px; - - &:hover { - color: var(--tree-expander-hover-color); - } - } - - .tree-expander, .tool-button { - cursor: pointer; - - * { - pointer-events: none; - } - } - - .tree-group { - &__header { - display: flex; - flex-flow: row; - align-items: center; - height: 1.3rem; - white-space: nowrap; - - &-content { - display: flex; - flex-flow: row; - align-items: center; - } - - input[type='checkbox'] { - cursor: pointer; - accent-color: var(--checkbox-checked-background-color); - } - - } - - &__count { - margin-left: 10px; - color: var(--tree-count-color); - } - - &__childs { - .tree-group__channel { - display: flex; - flex-flow: row; - align-items: center; - margin-left: 50px; - height: 1.3rem; - gap: 3px; - - &-tools { - display: flex; - flex-flow: row; - align-items: center; - gap: 3px; - - } - - &-content { - display: flex; - flex-flow: row; - align-items: center; - white-space: nowrap; - } - - &-nr { - min-width: 2rem; - font-size: 1rem; - margin-right: 8px; - text-align: right; - color: var(--tree-count-color); - - &:after { - content: ')'; - } - } - } - } - - &__header-content, &__channel-content { - cursor: pointer; - - &:hover { - background-color: var(--tree-hover-background-color); - color: var(--tree-hover-color); - - .tree-group__count { - color: var(--tree-hover-color); - } - } - } - } -} diff --git a/frontend/src/component/playlist-tree/playlist-tree.tsx b/frontend/src/component/playlist-tree/playlist-tree.tsx deleted file mode 100644 index f9c1eb6bd..000000000 --- a/frontend/src/component/playlist-tree/playlist-tree.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import React, {useCallback, useState, useRef, useEffect} from 'react'; -import './playlist-tree.scss'; -import {PlaylistCategories, PlaylistGroup, PlaylistItem} from "../../model/playlist"; -import copyToClipboard from "../../utils/clipboard"; -import {first} from "rxjs/operators"; -import {noop} from "rxjs"; -import {useSnackbar} from "notistack"; -import {getIconByName} from "../../icons/icons"; -import ServerConfig from "../../model/server-config"; -import {PlaylistRequest, PlaylistRequestType} from "../../model/playlist-request"; - -export type PlaylistTreeState = { [key: number]: boolean }; - -interface PlaylistTreeProps { - serverConfig: ServerConfig; - data: PlaylistCategories; - playlistRequest: PlaylistRequest; - state: PlaylistTreeState; - onCopy: (playlistItem: PlaylistItem) => void; - onPlay?: (playlistItem: PlaylistItem) => void; - onDownload?: (playlistItem: PlaylistItem) => void; - onWebSearch?: (playlistItem: PlaylistItem) => void; -} - -export default function PlaylistTree(props: PlaylistTreeProps) { - const {serverConfig, data, playlistRequest, onCopy, onPlay, onDownload, onWebSearch} = props; - - const [, setForceUpdate] = useState(undefined); - const expanded = useRef({}); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const [videoExtensions, setVideoExtensions] = useState([]); - - useEffect(() => { - if (serverConfig) { - setVideoExtensions(serverConfig.video?.extensions); - } - return noop; - }, [serverConfig]); - - const getPlaylistItemById = useCallback((itemId: string): PlaylistItem => { - const id = parseInt(itemId); - if (data && !isNaN(id)) { - const groups = [data?.live, data?.vod, data?.series].filter(Boolean).flat(); - for (let i = 0, len = groups.length; i < len; i++) { - const group = groups[i]; - for (let j = 0, clen = group.channels?.length ?? 0; j < clen; j++) { - const plitem = group.channels[j]; - // eslint-disable-next-line eqeqeq - if (plitem.id == id) { - return plitem; - } - } - } - } - return undefined; - }, [data]); - - const handleExpand = useCallback((event: any) => { - const key = event.target.dataset.group; - expanded.current[key] = !expanded.current[key]; - setForceUpdate({}); - }, []); - - const handleClipboardRealUrl = useCallback((e: any) => { - const item = getPlaylistItemById(e.target.dataset.item); - if (item) { - let virtual_id = undefined; - switch (playlistRequest.rtype) { - case PlaylistRequestType.TARGET: - virtual_id = item.virtual_id; - break; - case PlaylistRequestType.INPUT: - case PlaylistRequestType.XTREAM: - case PlaylistRequestType.M3U: - break; - } - if (virtual_id) { - onCopy(item); - copyToClipboard(''+virtual_id).pipe(first()).subscribe({ - next: value => enqueueSnackbar(value ? "URL copied to clipboard" : "Copy to clipboard failed!", {variant: value ? 'success' : 'error'}), - error: _ => enqueueSnackbar("Copy to clipboard failed!", {variant: 'error'}), - complete: noop, - }); - } - } - }, [enqueueSnackbar, getPlaylistItemById, onCopy, playlistRequest]); - - const handleClipboardUrl = useCallback((e: any) => { - const item = getPlaylistItemById(e.target.dataset.item); - if (item) { - - - onCopy(item); - copyToClipboard(item.url).pipe(first()).subscribe({ - next: value => enqueueSnackbar(value ? "URL copied to clipboard" : "Copy to clipboard failed!", {variant: value ? 'success' : 'error'}), - error: _ => enqueueSnackbar("Copy to clipboard failed!", {variant: 'error'}), - complete: noop, - }); - } - }, [enqueueSnackbar, getPlaylistItemById, onCopy]); - - const handleWebSearch = useCallback((e: any) => { - if (onWebSearch) { - const item = getPlaylistItemById(e.target.dataset.item); - if (item) { - onWebSearch(item); - } - } - }, [getPlaylistItemById, onWebSearch]); - - const handleDownloadUrl = useCallback((e: any) => { - if (onDownload) { - if (!serverConfig.video.download?.directory) { - enqueueSnackbar("Please updated the server configuration and add video.download directory and headers!", {variant: 'error'}) - } else { - const item = getPlaylistItemById(e.target.dataset.item); - if (item) { - onDownload(item); - } - } - } - }, [serverConfig, enqueueSnackbar, getPlaylistItemById, onDownload]); - - const handlePlayUrl = useCallback((e: any) => { - if (onPlay) { - const item = getPlaylistItemById(e.target.dataset.item); - if (item) { - onPlay(item); - } - } - }, [onPlay, getPlaylistItemById]); - - const isVideoFile = useCallback((entry: PlaylistItem): boolean => { - if (videoExtensions && entry.url) { - for (const ext of videoExtensions) { - if (entry.url.endsWith(ext)) { - return true; - } - } - } - return false; - }, [videoExtensions]); - - const renderEntry = useCallback((entry: PlaylistItem, index: number): React.ReactNode => { - return
-
-
- {getIconByName('LinkRounded')} -
-
- {getIconByName('LinkRounded')} -
- {onPlay &&
- {getIconByName('PlayArrow')} -
} - {isVideoFile(entry) && - <> -
- {getIconByName('Download')} -
- {serverConfig.video?.web_search && -
- {getIconByName('WebSearch')} -
- } - - } -
-
-
{index + 1}
- {entry.name}
-
- }, [onPlay, handleClipboardUrl, handlePlayUrl, handleDownloadUrl, isVideoFile, handleWebSearch, serverConfig, handleClipboardRealUrl]); - - const renderGroup = useCallback((group: PlaylistGroup): React.ReactNode => { - return
-
-
{getIconByName(expanded.current[group.id] ? - 'ExpandMore' : 'ChevronRight')}
-
- {group.name} -
({group.channels?.length})
-
-
- {expanded.current[group.id] && ( -
- {group.channels?.map(renderEntry)} -
)} -
; - }, [handleExpand, renderEntry]); - - const renderPlaylist = useCallback((): React.ReactNode => { - if (!data) { - return ; - } - return - {[data?.live, data?.vod, data?.series].filter(Boolean).flat().map(renderGroup)} - ; - }, [data, renderGroup]); - - return
{renderPlaylist()}
; -} \ No newline at end of file diff --git a/frontend/src/component/playlist-video/channel-view.scss b/frontend/src/component/playlist-video/channel-view.scss deleted file mode 100644 index 193537000..000000000 --- a/frontend/src/component/playlist-video/channel-view.scss +++ /dev/null @@ -1,206 +0,0 @@ -@use '../../scss/common'; - -$menu-width: 300px; -$menu-width-closed: -288px; -$item-height: 48px; - -.playlist-video { - display: block; - flex: 1 1 auto; - width: 100%; - height: 100%; - box-sizing: border-box; - position: relative; - overflow: hidden; -} - -.channel-view { - display: flex; - flex-flow: column; - box-sizing: border-box; - z-index: 2; - font-size: 1rem; - position: absolute; - top: 0; - left: 0; - bottom: 0; - overflow: hidden; - gap: 4px; - transition: left 0.4s ease; - width: $menu-width; - height: 100%; - background-color: transparent; - border: 1px solid var(--border-color); - - &__closed { - left: $menu-width-closed; - border: none; - cursor:pointer; - &:hover { - background-color: var(--card-background-color); - } - .channel-view__header, - .channel-view__content { - display: none; - } - } - - //&:hover { - // background-color: #00000088; - // border: 1px solid var(--border-color); - // width: $menu-width; - // .channel-view__content, - // .channel-view__header { - // display: flex; - // } - //} - - &__header { - display: flex; - flex-flow: row; - align-items: center; - box-sizing: border-box; - height: $item-height; - } - - &__menu { - display: flex; - flex-flow: row; - flex: 1 1 auto; - box-sizing: border-box; - align-items: center; - overflow: hidden; - cursor: pointer; - - &-title { - overflow: hidden; - white-space: nowrap; - } - - &-back, - &-title { - &:hover { - cursor: pointer; - color: var(--icon-button-hover-color); - background-color: var(--icon-button-hover-background-color); - } - } - - - &-disabled { - pointer-events: none; - } - - &-back { - &-disabled { - color: var(--card-background-color); - } - } - } - - &__categories { - display: flex; - flex-flow: row; - box-sizing: border-box; - gap: 4px; - - &-category { - border-radius: var(--border-radius); - padding: 2px 4px; - cursor: pointer; - &:hover { - color: var(--icon-button-hover-color); - background-color: var(--icon-button-hover-background-color); - } - } - } - - &__content { - display: flex; - flex-flow: column; - box-sizing: border-box; - overflow-x: hidden; - overflow-y: auto; - } - - &__groups { - display: flex; - flex-flow: column; - font-size: 1.2rem; - gap: 4px; - flex-flow: column; - box-sizing: border-box; - padding: 0 4px; - - &-group { - display: flex; - flex-flow: row; - cursor: pointer; - border: 1px solid var(--border-color); - background-color: var(--card-background-color); - flex-flow: row nowrap; - align-items: center; - align-content: center; - box-sizing: border-box; - gap: 8px; - height: $item-height; - overflow: hidden; - padding: 0 4px; - - &-count { - color: var(--hint-color); - } - &:hover { - color: var(--icon-button-hover-color); - background-color: var(--icon-button-hover-background-color); - } - } - } - - &__channels { - display: flex; - flex-flow: column; - gap: 4px; - box-sizing: border-box; - padding: 0 4px; - &-channel { - display: flex; - border: 1px solid var(--border-color); - background-color: var(--card-background-color); - flex-flow: row nowrap; - align-items: center; - align-content: center; - box-sizing: border-box; - gap: 8px; - height: $item-height; - cursor: pointer; - overflow: hidden; - - &:hover { - color: var(--icon-button-hover-color); - background-color: var(--icon-button-hover-background-color); - } - &-name { - display: flex; - flex-flow: row nowrap; - white-space: nowrap; - box-sizing: border-box; - } - &-logo { - box-sizing: border-box; - display: flex; - align-items: center; - align-content: center; - width: 42px; - height: 42px; - min-width: 42px; - min-height: 42px; - img { - box-sizing: border-box; - width: 100%; - aspect-ratio: auto; - } - } - } - } -} diff --git a/frontend/src/component/playlist-video/channel-view.tsx b/frontend/src/component/playlist-video/channel-view.tsx deleted file mode 100644 index f49a3aa42..000000000 --- a/frontend/src/component/playlist-video/channel-view.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import {PlaylistCategories, PlaylistCategory, PlaylistGroup, PlaylistItem} from "../../model/playlist"; -import './channel-view.scss'; -import React, {ReactNode, useCallback, useState} from "react"; -import {getIconByName} from "../../icons/icons"; -import useTranslator from "../../hook/use-translator"; -import {set} from "react-datepicker/dist/date_utils"; - -const getLogo = (item: PlaylistItem): string | undefined => { - if (item.logo_small && item.logo_small.length > 0) { - return item.logo_small; - } - if (item.logo && item.logo.length > 0) { - return item.logo; - } - return undefined; -} - -interface ChannelViewProps { - data: PlaylistCategories; - onPlay?: (playlistItem: PlaylistItem) => void; -} - -export default function ChannelView(props: ChannelViewProps) { - - const {data, onPlay} = props; - const translate = useTranslator(); - const [selectedItem, setSelectedItem] = useState(); - const [selectedGroup, setSelectedGroup] = useState(); - const [selectedCategory, setSelectedCategory] = useState(PlaylistCategory.LIVE); - const [openChannelList, setOpenChannelList] = useState(true); - - const handleCategorySelect = useCallback((evt: any) => { - let category = evt.target.dataset.category; - if ((data as any)?.[category]) { - setSelectedCategory(category); - } - }, [data]); - - const handleGroupSelect = useCallback((evt: any) => { - let category = evt.target.dataset.category; - let groupId = evt.target.dataset.group; - let groups: PlaylistGroup[] = (data as any)[category]; - if (groups) { - // eslint-disable-next-line eqeqeq - let group = groups.find(grp => grp.id == groupId); - setSelectedGroup(group) - } - }, [data]); - - const handleChannelSelect = useCallback((channel: PlaylistItem) => { - if (channel) { - setSelectedItem(channel) - onPlay?.(channel); - } - }, [onPlay]); - - const handleCloseChannelViewState = useCallback(() => { - setOpenChannelList(false); - }, []); - - - const handleOpenChannelViewState = useCallback(() => { - if (!openChannelList) { - setOpenChannelList(true); - } - }, [openChannelList]); - - const handleBack = useCallback(() => { - if (selectedGroup) { - setSelectedGroup(undefined); - return; - } - }, [selectedGroup]); - - const renderItems = useCallback((category: PlaylistCategory, group: PlaylistGroup): ReactNode => { - return
- {group.channels.map(channel => ( -
handleChannelSelect(channel)} - className={'channel-view__channels-channel channel-' + channel.xtream_cluster}> -
- {getLogo(channel) && {"logo"} (e.target.onerror = null) || (e.target.src = "assets/missing-logo.svg")}/>} -
-
- {channel.title} -
-
- ))} -
; - }, [handleChannelSelect]); - - const renderGroups = useCallback((category: PlaylistCategory, groups: PlaylistGroup[]): ReactNode => { - return
- {groups.map(group => ( -
- {group.name} - ({(group.channels?.length ?? 0)}) -
- ))} -
; - }, [handleGroupSelect]); - - const renderCategories = useCallback(() => { - return
- {[['live', 'Live'], ['vod', 'VOD'], ['series', 'Series']].map(cat =>
{getIconByName(cat[1])}
)} -
; - }, [data, handleCategorySelect]); - - const renderSelectionContent = useCallback((): ReactNode => { - // if (selectedItem) { - // return renderItem(selectedItem); - // } - if (selectedGroup) { - return renderItems(selectedCategory, selectedGroup); - } - if (selectedCategory) { - return renderGroups(selectedCategory, data[selectedCategory]); - } - return
{translate("MESSAGES.NO_CONTENT")}
- }, [data, selectedCategory, selectedGroup, - renderGroups, renderItems, translate]) - - const renderContent = useCallback((): ReactNode => { - if (data) { - if (selectedItem || selectedGroup || selectedCategory) { - return
- {renderSelectionContent()} -
; - } - } - return
{translate("MESSAGES.NO_CONTENT")}
; - }, [data, selectedCategory, selectedGroup, selectedItem, renderSelectionContent, translate]); - - const renderMenu = useCallback((): ReactNode => { - return
-
{getIconByName('Clear')}
- {selectedGroup && -
{getIconByName('Back')}
- } -
{selectedGroup?.name ?? ''}
-
- }, [selectedGroup, handleBack, handleCloseChannelViewState]) - - return
-
- {renderMenu()} - {renderCategories()} -
- {renderContent()} -
-} diff --git a/frontend/src/component/playlist-video/playlist-video.tsx b/frontend/src/component/playlist-video/playlist-video.tsx deleted file mode 100644 index 61b759785..000000000 --- a/frontend/src/component/playlist-video/playlist-video.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, {JSX, useRef} from "react"; -import VideoPlayer from "../video-player/video-player"; -import {PlaylistCategories, PlaylistItem} from "../../model/playlist"; -import {Observable} from "rxjs"; -import {PlaylistRequest} from "../../model/playlist-request"; -import ChannelView from "./channel-view"; - -interface PlaylistVideoProps { - data: PlaylistCategories; - channel: Observable<[PlaylistItem, PlaylistRequest]>; - onPlay?: (playlistItem: PlaylistItem) => void; -} - -export default function PlaylistVideo(props: PlaylistVideoProps): JSX.Element { - const {data, channel, onPlay} = props; - const playerRef = useRef(undefined); - const handlePlayerReady = (player: any) => { - playerRef.current = player; - }; - - return
- - -
; -} \ No newline at end of file diff --git a/frontend/src/component/playlist-viewer/playlist-viewer.scss b/frontend/src/component/playlist-viewer/playlist-viewer.scss deleted file mode 100755 index 843f11a3f..000000000 --- a/frontend/src/component/playlist-viewer/playlist-viewer.scss +++ /dev/null @@ -1,39 +0,0 @@ -.playlist-viewer { - display: flex; - flex-flow: column; - flex: 1 1 auto; - overflow: hidden; - padding: 10px; - height: 100%; - color: var(--playlist-color); - box-sizing: border-box; - - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - - &__header { - display: flex; - flex-flow: row; - align-items: center; - justify-content: flex-end; - padding-bottom: 4px; - } - - &__content { - display: flex; - flex-flow: column; - flex: 1 1 auto; - overflow: auto; - box-sizing: border-box; - } - - .group-label { - display: flex; - flex-flow: row nowrap; - align-items: center; - - .group-label-text { - width: 400px; - } - } -} \ No newline at end of file diff --git a/frontend/src/component/playlist-viewer/playlist-viewer.tsx b/frontend/src/component/playlist-viewer/playlist-viewer.tsx deleted file mode 100755 index 2cbf883ad..000000000 --- a/frontend/src/component/playlist-viewer/playlist-viewer.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import React, {useMemo, useEffect, useState, useCallback, ReactNode} from "react"; -import './playlist-viewer.scss'; -import PlaylistTree, {PlaylistTreeState} from "../playlist-tree/playlist-tree"; -import {Observable, noop, finalize} from "rxjs"; -import ServerConfig from "../../model/server-config"; -import PlaylistGallery from "../playlist-gallery/playlist-gallery"; -import {getIconByName} from "../../icons/icons"; -import { - EmptyPlaylistCategories, - PlaylistCategories, - PlaylistGroup, - PlaylistItem -} from "../../model/playlist"; -import {useSnackbar} from "notistack"; -import {first} from "rxjs/operators"; -import PlaylistVideo from "../playlist-video/playlist-video"; -import {PlaylistRequest} from "../../model/playlist-request"; - -function textMatch(text: string, searchRequest: SearchRequest): boolean { - if (searchRequest.regexp) { - // eslint-disable-next-line eqeqeq - return text.match(searchRequest.filter) != undefined; - } else { - return (text.toLowerCase().indexOf(searchRequest.filter) > -1); - } -} - -function filterMatchingChannels(grp: PlaylistGroup, searchRequest: SearchRequest): PlaylistGroup { - let channels: PlaylistItem[] = []; - for (const c of grp.channels) { - if (textMatch(c.name, searchRequest)) { - channels.push(c); - } - } - if (channels.length) { - return { - id: grp.id, - name: grp.name, - channels - } as PlaylistGroup; - } - return undefined; -} - -function filterMatchingGroups(playlistCategories: PlaylistCategories, searchRequest: SearchRequest): Observable { - return new Observable((observer) => { - searchRequest.filter = searchRequest.regexp ? searchRequest.filter : searchRequest.filter.toLowerCase(); - const result: PlaylistCategories = { - live:[], - vod: [], - series: [], - }; - for (const category of ['live', 'vod', 'series']) { - const groups = (playlistCategories as any)[category] ?? []; - const currentResult = (result as any)[category]; - for (const g of groups) { - if (textMatch(g.name, searchRequest)) { - currentResult.push(g); - } else { - const matches = filterMatchingChannels(g, searchRequest); - if (matches) { - currentResult.push(matches); - } - } - } - } - if (result.live.length || result.vod.length || result.series.length) { - observer.next(result); - } else { - observer.next(undefined); - } - observer.complete(); - }) -} - -type PlaylistViewType = 'tree' | 'gallery' | 'player'; - -const getToggleIcon = (view: PlaylistViewType): ReactNode => { - switch (view) { - case 'gallery': return getIconByName('Live'); - case 'player': return getIconByName('TreeView'); - default: return getIconByName('Gallery'); - } -} - - -export interface SearchRequest { - filter: string, - regexp: boolean -} - - - -export interface IPlaylistViewer { - getFilteredPlaylist: () => PlaylistCategories; -} - -interface PlaylistViewerProps { - serverConfig: ServerConfig; - playlist: PlaylistCategories; - searchChannel: Observable; - videoChannel: Observable<[PlaylistItem, PlaylistRequest]>; - playlistRequest: PlaylistRequest; - onProgress: (value: boolean) => void; - onCopy: (playlistItem: PlaylistItem) => void; - onPlay?: (playlistItem: PlaylistItem) => void; - onDownload?: (playlistItem: PlaylistItem) => void; - onWebSearch?: (playlistItem: PlaylistItem) => void; -} - -export default function PlaylistViewer(props: PlaylistViewerProps) { - const {serverConfig, playlist, playlistRequest, searchChannel, videoChannel, - onProgress, onCopy, onPlay, onDownload, onWebSearch} = props; - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const [data, setData] = useState(EmptyPlaylistCategories); - const [galleryView, setGalleryView] = useState((localStorage.getItem("galleryView") as any) ?? 'tree'); - const checked = useMemo((): PlaylistTreeState => ({}), []); - - useEffect(() => { - setData(playlist); - return noop; - }, [playlist]); - - useEffect(() => { - const sub = searchChannel.subscribe((searchRequest: SearchRequest) => { - let criteria = searchRequest.filter; - // eslint-disable-next-line eqeqeq - if (criteria == undefined || !criteria.length || !criteria.trim().length) { - setData(playlist); - } else { - const trimmedCrit = criteria.trim(); - if (trimmedCrit.length < 2) { - enqueueSnackbar("Minimum search criteria length is 2", {variant: 'info'}); - } else { - searchRequest.filter = trimmedCrit; - onProgress && onProgress(true) - filterMatchingGroups(playlist, searchRequest).pipe( - finalize(() => onProgress && onProgress(false)), - first()) - .subscribe((matches: PlaylistCategories) => { - if (matches) { - const foundCount = matches.live.length + matches.vod.length + matches.series.length; - enqueueSnackbar("Found Entries: " + foundCount, {variant: 'success'}); - setData(matches); - } else { - enqueueSnackbar("Nothing found!", {variant: 'info'}); - } - }); - } - } - }); - return () => sub.unsubscribe(); - }, [searchChannel, playlist, enqueueSnackbar, onProgress]); - - const renderContent = () => { - switch (galleryView) { - case 'gallery' : - return - case 'player' : - return - default: - return - } - } - - const toggleView = useCallback(() => { - const getNext = (data: string) => { - switch (data) { - case 'tree': return 'gallery'; - case 'gallery': return 'player'; - case 'player': return 'tree'; - } - return 'gallery'; - }; - - setGalleryView((data: any) => { - let view: any = getNext(data); - localStorage.setItem("galleryView", view); - return view; - }); - }, []); - - return
-
-
- {getToggleIcon(galleryView)} -
-
-
- {renderContent()} -
-
-} \ No newline at end of file diff --git a/frontend/src/component/popup-menu/popup-menu.scss b/frontend/src/component/popup-menu/popup-menu.scss deleted file mode 100644 index 6fbb4c374..000000000 --- a/frontend/src/component/popup-menu/popup-menu.scss +++ /dev/null @@ -1,14 +0,0 @@ -@use '../../scss/common'; - -.popup-menu { - position: fixed; - z-index: 9999; - color: var(--popup-color); - background-color: var(--popup-background-color); - border: 1px solid var(--popup-border-color); - border-radius: var(--border-radius); - padding: 4px; - filter: drop-shadow(8px 9px 7px var(--popup-shadow-color)); - - @include common.list-hover(); -} \ No newline at end of file diff --git a/frontend/src/component/popup-menu/popup-menu.tsx b/frontend/src/component/popup-menu/popup-menu.tsx deleted file mode 100644 index fed6d3753..000000000 --- a/frontend/src/component/popup-menu/popup-menu.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, {ReactNode, useEffect, useRef, useState} from "react"; - -import './popup-menu.scss'; -import ClickAwayListener from "../../utils/click-away-listener"; - -interface PopupMenuProps { - position: { x: number, y: number }, - children: ReactNode, - onHide: () => void; -} - -export default function PopupMenu(props: PopupMenuProps) { - const {position, onHide} = props; - const popupRef = useRef(undefined); - const [popupPosition, setPopupPosition] = useState({top: 0, left: -2000}) - - useEffect(() => { - if (position && popupRef.current) { - const ww = window.innerWidth; - const {offsetWidth} = popupRef.current; - const style: any = - (position.x + offsetWidth > ww) - ? {right: 12, top: position.y} - : {left: position.x, top: position.y}; - setPopupPosition(style); - } - }, [position]) - - return <>{props.position && -
- {props.children} -
-
} -} \ No newline at end of file diff --git a/frontend/src/component/preferences/preferences.scss b/frontend/src/component/preferences/preferences.scss deleted file mode 100644 index e514d7b45..000000000 --- a/frontend/src/component/preferences/preferences.scss +++ /dev/null @@ -1,102 +0,0 @@ -@use '../../scss/common'; - -.preferences { - @include common.container_horz_no_padding(); - overflow: hidden; - flex: 1 1 auto; - - &__content { - @include common.container_horz_nowrap(flex-start, 0); - flex: 1 1 auto; - overflow: hidden; - gap: 0; - } - - &__panels { - @include common.container_vert_no_padding(flex-start); - overflow: hidden; - flex: 1 1 auto; - - .panel { - @include common.container_vert(); - flex: 1 1 auto; - overflow: hidden; - .card { - @include common.container_vert(); - flex: 1 1 auto; - overflow: hidden; - } - } - } - - &__sidebar { - display: flex; - flex-flow: column; - box-sizing: border-box; - @media (max-width: common.$media-size-mobile) { - flex-flow: row; - } - //max-width: 48px; - //min-width: 48px; - //width: 48px; - @media (max-width: common.$media-size-mobile) { - max-width: 32px; - min-width: 32px; - } - gap: 12px; - padding: 8px 4px; - - button { - @include common.iconButton(); - border: 1px solid transparent; - } - - button.selected { - color: var(--icon-button-hover-color); - background-color: var(--icon-button-hover-background-color); - border: 1px solid var(--icon-button-hover-border-color); - } - - &-menu-action { - display: flex; - flex-flow: row nowrap; - gap: 8px; - align-items: center; - cursor: pointer; - color: var(--menu-button-color); - background-color: var(--menu-button-background-color); - border: 1px solid transparent; - border-radius: var(--border-radius); - padding: 8px 16px; - user-select: none; - white-space: nowrap; - - &_update { svg { fill: var(--menu-icon-color-update); } } - &_user { svg { fill: var(--menu-icon-color-user); } } - &_api_server { svg { fill: var(--menu-icon-color-api_server); } } - &_main_config { svg { fill: var(--menu-icon-color-main_config); } } - &_status { svg { fill: var(--menu-icon-color-status); } } - - - * { - pointer-events: none; - } - - @media (max-width: common.$media-size-mobile) { - label { - display: none; - } - } - - &:hover { - border: 1px solid var(--icon-button-hover-border-color); - } - } - - &-menu-action.selected { - color: var(--menu-button-hover-color); - background-color: var(--menu-button-hover-background-color); - } - } - -} \ No newline at end of file diff --git a/frontend/src/component/preferences/preferences.tsx b/frontend/src/component/preferences/preferences.tsx deleted file mode 100644 index 3112739b8..000000000 --- a/frontend/src/component/preferences/preferences.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, {useCallback, useMemo, useState} from "react"; -import './preferences.scss'; -import ServerConfig from "../../model/server-config"; -import UserView from "../user-view/user-view"; -import TargetUpdateView from "../target-update-view/target-update-view"; -import ApiProxyView from "../api-proxy-view/api-proxy-view"; -import {getIconByName} from "../../icons/icons"; -import Panel from "../panel/panel"; -import MainConfigView from "../main-config-view/main-config-view"; -import useTranslator from "../../hook/use-translator"; -import StatusView from "../status-view/status-view"; - -enum SidebarAction { - Update = 'update', - User = 'user', - ApiServer = 'api_server', - MainConfig = 'main_config', - Status = 'status', -} - -const SIDEBAR_ACTIONS: { action: SidebarAction, icon: string, label: string }[] = [ - {action: SidebarAction.Update, icon: 'Refresh', label: 'LABEL.REFRESH'}, - {action: SidebarAction.User, icon: 'User', label: 'LABEL.USERS'}, - {action: SidebarAction.ApiServer, icon: 'ApiServer', label: 'LABEL.PROXY'}, - {action: SidebarAction.MainConfig, icon: 'Config', label: 'LABEL.CONFIG'}, - {action: SidebarAction.Status, icon: 'Status', label: 'LABEL.STATUS'}, -]; - -interface PreferencesProps { - config: ServerConfig -} - -export default function Preferences(props: PreferencesProps) { - const {config} = props; - const translate = useTranslator(); - const [activePage, setActivePage] = useState(SidebarAction.Update); - const sidebar_actions = useMemo(() => SIDEBAR_ACTIONS.map(a => ({...a, label: translate(a.label)})), [translate]) - - const handleSidebarAction = useCallback((event: any) => { - const action = event.target.dataset.action; - if (action) { - setActivePage(action); - } - }, []); - - return
-
-
- {sidebar_actions.map(action => -
{getIconByName(action.icon)}
)} -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
-} \ No newline at end of file diff --git a/frontend/src/component/progress/progress.scss b/frontend/src/component/progress/progress.scss deleted file mode 100644 index be758a9bc..000000000 --- a/frontend/src/component/progress/progress.scss +++ /dev/null @@ -1,59 +0,0 @@ -.progress { - display: none; - position: fixed; - width: 100%; - justify-content: center; - color: aqua; - z-index: 100; -} - -.progress-visible { - display: flex; -} - -.spinner { - display: inline-block; - position: relative; - width: 80px; - height: 40px; -} - -.spinner div { - display: inline-block; - position: absolute; - left: 8px; - width: 16px; - min-width: 16px; - max-width: 16px; - min-height: 16px; - max-height: 16px; - border-radius: 50%; - background: var(--spinner-color); - opacity: 0.4; - animation: spinner 1s infinite alternate; -} - -.spinner div:nth-child(1) { - left: 8px; - animation-delay: 0s; -} - -.spinner div:nth-child(2) { - left: 32px; - animation-delay: -0.3s; -} - -.spinner div:nth-child(3) { - left: 56px; - animation-delay: -0.6s; -} - -@keyframes spinner { - 0% { - transform: translateY(0); - background-color: var(--spinner-highlight-color); - } - 100% { - transform: translateY(20px); - } -} diff --git a/frontend/src/component/progress/progress.tsx b/frontend/src/component/progress/progress.tsx deleted file mode 100644 index df21c88b5..000000000 --- a/frontend/src/component/progress/progress.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import './progress.scss'; - -interface ProgressProps { - visible: boolean; -} - -export default function Progress(props: ProgressProps) { - - return
-
-
-
-
-
-
; -} \ No newline at end of file diff --git a/frontend/src/component/proxy-select/proxy-select.scss b/frontend/src/component/proxy-select/proxy-select.scss deleted file mode 100644 index 6fff0c224..000000000 --- a/frontend/src/component/proxy-select/proxy-select.scss +++ /dev/null @@ -1,52 +0,0 @@ -.proxy-select { - display: flex; - flex-flow: row wrap; - gap: 4px; - padding: 2px; - - &__tag { - display: flex; - flex-flow: column; - justify-content: center; - align-items: center; - gap: 5px; - background-color: transparent; - color: var(--text-color); - padding: 3px 6px; - border-radius: 4px; - cursor: pointer; - border: 1px solid var(--border-light-color); - - div { - display: flex; - flex-flow: row nowrap; - justify-content: center; - align-items: center; - gap: 5px; - } - - &-sub { - border-radius: 2px; - border: 1px solid transparent; - padding: 2px 4px; - &:hover { - border: 1px solid var(--option-active-background-color); - } - - } - - &-sub.selected { - color: var(--option-active-color); - background-color: var(--option-active-background-color); - } - - &:hover { - border: 1px solid var(--checkbox-checked-background-color); - } - - &-selected { - background-color: var(--checkbox-checked-background-color); - color: var(--text-color); - } - } -} \ No newline at end of file diff --git a/frontend/src/component/proxy-select/proxy-select.tsx b/frontend/src/component/proxy-select/proxy-select.tsx deleted file mode 100644 index 4f8383f90..000000000 --- a/frontend/src/component/proxy-select/proxy-select.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import './proxy-select.scss'; -import {useEffect, useState} from "react"; -import {noop} from "rxjs"; - -const getSubsStr = (subs: any): string => { - let result = Object.keys(subs).filter(key => subs[key]); - if (result.length > 0) {// && result.length < 3) { - return '[' + result.join(',') + ']'; - } - return ''; -}; - -const getSubs = (value: string) => { - if (value?.startsWith('reverse')) { - let result = {live:true, vod:true, series: true}; - if (!value.includes('live')) { - result.live = false; - } - if (!value.includes('vod')) { - result.vod = false; - } - if (!value.includes('series')) { - result.series = false; - } - if (result.live === false && result.vod === false && result.series === false) { - return {live:true, vod:true, series: true}; - } - return result; - } - return {live: false, vod:false, series: false}; -} - -interface ProxySelectProps { - name: string; - value: string; - onSelect: (name: string, values: any) => void; -} - -export default function ProxySelect(props: ProxySelectProps) { - const {name, value, onSelect} = props; - const [proxyType, setProxyType] = useState(() => value == undefined ? 0 : (value.startsWith('redirect')) ? 2 : 1); - const [subs, setSubs] = useState(() => getSubs(value)); - - useEffect(() => { - if (value) { - setProxyType(value.startsWith('redirect') ? 2 : 1); - setSubs(getSubs(value)); - } - return noop; - }, [value]); - - const changeProxyType = (pt: number) => { - setProxyType(pt); - onSelect(name, pt === 1 ? ('reverse' + getSubsStr(subs)) : 'redirect'); - }; - - const toggle = (evt: any, sub: string)=> { - evt.preventDefault(); - evt.stopPropagation(); - setSubs((subs: any) => { - subs[sub] = !subs[sub]; - onSelect(name, proxyType === 1 ? ('reverse' + getSubsStr(subs)) : 'redirect'); - return {...subs}; - }); - }; - - return
-
changeProxyType(1)}> -
Reverse
-
- toggle(evt, 'live')}>Live - toggle(evt, 'vod')}>Vod - toggle(evt, 'series')}>Series -
-
-
changeProxyType(2)}> - Redirect -
-
-} \ No newline at end of file diff --git a/frontend/src/component/schedule-editor/schedule-editor.scss b/frontend/src/component/schedule-editor/schedule-editor.scss deleted file mode 100644 index 7ff04fcc3..000000000 --- a/frontend/src/component/schedule-editor/schedule-editor.scss +++ /dev/null @@ -1,37 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - -$input-width: 150px; - -.schedule-editor { - display: flex; - flex-flow: column; - box-sizing: border-box; - gap: 2px; - - &__schedule { - display: flex; - flex-flow: row; - align-items: center; - padding: 6px; - height: 1.5rem; - - input { - width: $input-width; - min-width: $input-width; - max-width: $input-width; - } - - - &-toolbar { - display: flex; - flex-flow: row nowrap; - justify-content: flex-start; - gap: 2px; - } - } - button { - @include common.iconButton(); - } - -} diff --git a/frontend/src/component/schedule-editor/schedule-editor.tsx b/frontend/src/component/schedule-editor/schedule-editor.tsx deleted file mode 100644 index 5551d958d..000000000 --- a/frontend/src/component/schedule-editor/schedule-editor.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, {JSX, useCallback, useEffect, useMemo, useState} from "react"; -import "./schedule-editor.scss"; -import {Schedule, SourceConfig} from "../../model/server-config"; -import TagSelect from "../tag-select/tags-select"; -import {getIconByName} from "../../icons/icons"; - -interface ScheduleEditorProps { - name: string; - values: Schedule[], - sources: SourceConfig[]; - onChange: (name: string, values: Schedule[]) => void; -} - -export default function ScheduleEditor(props: ScheduleEditorProps) { - const { name, values, sources, onChange} = props; - const targets = useMemo(() => sources.flatMap(s => s.targets) - .filter(Boolean).flatMap(t => ({value: t.name, label: t.name})), [sources]); - const [schedules, setSchedules] = useState([]); - - useEffect(() => { - const schedulesCopy = values?.map(s => ({...s})) || []; - setSchedules(schedulesCopy); - }, [values]) - - const handleTargetChange = useCallback((field: string, selected: any):void => { - setSchedules(data => { - data[field as any].targets = selected; - onChange(name, data); - return data; - }); - }, [name, onChange]); - - const handleScheduleChange = useCallback((evt: any) => { - const field = evt.target.dataset.field; - setSchedules(data => { - data[field as any].schedule = evt.target.value; - onChange(name, data); - return data; - }); - - }, [name, onChange]); - - const handleScheduleRemove = useCallback((evt: any) => { - const index = evt.target.dataset.index; - setSchedules(data => { - data.splice(index, 1); - let newData = [...data]; - onChange(name, newData); - return newData; - }); - }, [name, onChange]); - - const handleScheduleAdd = useCallback((evt: any) => { - setSchedules(data => { - let newData = [...data, {schedule: '', targets: undefined}]; - onChange(name, newData); - return newData; - }); - }, [name, onChange]); - - let lastIndex = (schedules?.length ?? 0) -1; - - const renderSchedule = useCallback((schedule: Schedule, index: number): JSX.Element => { - return
- - -
- - {index === lastIndex && - } -
-
- }, [targets, handleScheduleChange, handleTargetChange, handleScheduleRemove, handleScheduleAdd, lastIndex]); - - return ( -
- {schedules?.map(renderSchedule)} - {lastIndex < 0 && - } -
- ); -} diff --git a/frontend/src/component/setup-wizard/_setup-common.scss b/frontend/src/component/setup-wizard/_setup-common.scss deleted file mode 100644 index daec94e52..000000000 --- a/frontend/src/component/setup-wizard/_setup-common.scss +++ /dev/null @@ -1,45 +0,0 @@ -@use '../../scss/common' as common; - -.setup-label { - -} - -.wizard-page { - display: flex; - flex-flow: column; - overflow: hidden; - - &__header-title { - display: flex; - color: var(--title-label-color); - justify-content: center; - } - - &__content { - display: flex; - flex-flow: column; - overflow: auto; - } - - &__toolbar { - display: flex; - flex-flow: row; - box-sizing: border-box; - align-items: center; - justify-content: center; - gap: 4px; - - svg { - fill: var(--icon-button-color); - } - - button { - @include common.textButton(); - } - - .finish-button { - color: var(--option-active-color); - background-color: var(--option-active-background-color); - } - } -} \ No newline at end of file diff --git a/frontend/src/component/setup-wizard/progress-bar/progress-bar.scss b/frontend/src/component/setup-wizard/progress-bar/progress-bar.scss deleted file mode 100644 index aa436fcd4..000000000 --- a/frontend/src/component/setup-wizard/progress-bar/progress-bar.scss +++ /dev/null @@ -1,3 +0,0 @@ -.progress-bar { - -} \ No newline at end of file diff --git a/frontend/src/component/setup-wizard/progress-bar/progress-bar.tsx b/frontend/src/component/setup-wizard/progress-bar/progress-bar.tsx deleted file mode 100644 index cafcdd364..000000000 --- a/frontend/src/component/setup-wizard/progress-bar/progress-bar.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import './progress-bar.scss'; -import {JSX} from "react"; -interface ProgressBarProps { - step: number; - total: number; -} -export default function ProgressBar(props: ProgressBarProps): JSX.Element{ - const {step, total} = props; - - return
-
-
; -} diff --git a/frontend/src/component/setup-wizard/setup-wizard.scss b/frontend/src/component/setup-wizard/setup-wizard.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/component/setup-wizard/setup-wizard.tsx b/frontend/src/component/setup-wizard/setup-wizard.tsx deleted file mode 100644 index dfc8decb8..000000000 --- a/frontend/src/component/setup-wizard/setup-wizard.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useState } from "react"; -import "./setup-wizard.scss"; -import "./step-welcome/step-welcome"; -import StepWelcome from "./step-welcome/step-welcome"; -import StepSummary from "./step-summary/step-summary"; -import ProgressBar from "./progress-bar/progress-bar"; - -interface SetupWizardProps { -} - -export default function SetupWizard(props: SetupWizardProps) { - const [step, setStep] = useState(0); - - const steps = [ - setStep(step + 1)} />, - // setStep(step + 1)} onBack={() => setStep(step - 1)} />, - // setStep(step + 1)} onBack={() => setStep(step - 1)} />, - console.log('Finished!')} onBack={() => setStep(step - 1)} />, - ]; - - return ( -
- {steps[step]} - -
- ); -} \ No newline at end of file diff --git a/frontend/src/component/setup-wizard/step-summary/step-summary.scss b/frontend/src/component/setup-wizard/step-summary/step-summary.scss deleted file mode 100644 index 75d3cba9f..000000000 --- a/frontend/src/component/setup-wizard/step-summary/step-summary.scss +++ /dev/null @@ -1,5 +0,0 @@ -@use '../setup-common'; - -.setup-summary { - -} \ No newline at end of file diff --git a/frontend/src/component/setup-wizard/step-summary/step-summary.tsx b/frontend/src/component/setup-wizard/step-summary/step-summary.tsx deleted file mode 100644 index a6230a63f..000000000 --- a/frontend/src/component/setup-wizard/step-summary/step-summary.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import "./step-summary.scss"; -import useTranslator from "../../../hook/use-translator"; - -interface StepSummaryProps { - onFinish: () => void; - onBack: () => void; -} - -export default function StepSummary(props: StepSummaryProps) { - - const { onFinish, onBack } = props; - - - const translate = useTranslator(); - - return
-

{translate("SETUP.MSG.WELCOME")}

-
- - -
-
; -} diff --git a/frontend/src/component/setup-wizard/step-welcome/step-welcome.scss b/frontend/src/component/setup-wizard/step-welcome/step-welcome.scss deleted file mode 100644 index 255a79559..000000000 --- a/frontend/src/component/setup-wizard/step-welcome/step-welcome.scss +++ /dev/null @@ -1,9 +0,0 @@ -@use '../setup-common'; - -.step-welcome { - - img { - max-height: 500px; - } - -} \ No newline at end of file diff --git a/frontend/src/component/setup-wizard/step-welcome/step-welcome.tsx b/frontend/src/component/setup-wizard/step-welcome/step-welcome.tsx deleted file mode 100644 index 46abcfc8c..000000000 --- a/frontend/src/component/setup-wizard/step-welcome/step-welcome.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import "./step-welcome.scss"; -import useTranslator from "../../../hook/use-translator"; - -interface StepWelcomeProps { - onNext: () => void; -} - -export default function StepWelcome(props: StepWelcomeProps) { - const {onNext} = props; - - const translate = useTranslator(); - - return
-

{translate("SETUP.MSG.WELCOME")}

-
- -
-
- -
-
; -} diff --git a/frontend/src/component/sidebar/sidebar.scss b/frontend/src/component/sidebar/sidebar.scss deleted file mode 100644 index ff9b33030..000000000 --- a/frontend/src/component/sidebar/sidebar.scss +++ /dev/null @@ -1,34 +0,0 @@ -@use '../../scss/theme'; -$toggle-size: 24px; - -.sidebar { - display: flex; - flex-flow: column; - justify-content: center; - flex: 0 1 auto; - - .sidebar-content { - display: flex; - flex-flow: column; - justify-content: center; - flex: 1 1 auto; - overflow: hidden; - } - .sidebar-collapsed { - max-height: 0; - height: 0; - overflow: hidden; - } - - .sidebar-toggle { - display: flex; - align-items: center; - justify-content: center; - color: var(--sidebar-toggle-color); - cursor: pointer; - - &:hover { - background-color: var(--sidebar-toggle-hover-color); - } - } -} diff --git a/frontend/src/component/sidebar/sidebar.tsx b/frontend/src/component/sidebar/sidebar.tsx deleted file mode 100644 index fc0bb85ba..000000000 --- a/frontend/src/component/sidebar/sidebar.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, {JSX, useCallback, useState} from "react"; -import './sidebar.scss'; -import {getIconByName} from "../../icons/icons"; -/* eslint-disable @typescript-eslint/no-empty-interface */ -interface SidebarProps { - children?: any; -} - -export default function Sidebar(props: SidebarProps): JSX.Element { - - const {children} = props; - const [collapsed, setCollapsed] = useState(true); - - const handleToggle = useCallback(() => { - setCollapsed(value => !value); - }, []); - - return
-
- {children} -
-
- {getIconByName(collapsed ? 'ArrowDown' : 'ArrowUp')} -
-
; - -} diff --git a/frontend/src/component/source-selector-editor/source-selector-editor.scss b/frontend/src/component/source-selector-editor/source-selector-editor.scss deleted file mode 100644 index 947d7d881..000000000 --- a/frontend/src/component/source-selector-editor/source-selector-editor.scss +++ /dev/null @@ -1,37 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - -.source-selector-editor { - & { - display: flex; - flex-flow: column; - gap: 12px; - min-height: 400px; - @include preferences.prefsToolbar(); - } - - &__content { - display: flex; - padding: 12px; - flex-flow: column; - flex: 1 1 auto; - min-width: 340px; - .form-view__col-label { - justify-content: flex-start; - min-width: 80px; - width: fit-content; - } - - .tab-set { - margin-bottom: 8px; - } - - &-tags, &-form { - display: flex; - flex-flow: column; - flex: 1 1 auto; - gap: 12px; - } - } -} - diff --git a/frontend/src/component/source-selector-editor/source-selector-editor.tsx b/frontend/src/component/source-selector-editor/source-selector-editor.tsx deleted file mode 100644 index 35812a4e0..000000000 --- a/frontend/src/component/source-selector-editor/source-selector-editor.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react"; -import './source-selector-editor.scss'; -import useTranslator from "../../hook/use-translator"; -import FormView, {FormFieldType} from "../form-view/from-view"; -import ServerConfig from "../../model/server-config"; -import {noop} from "rxjs"; -import TagSelect from "../tag-select/tags-select"; -import TabSet from "../tab-set/tab-set"; -import {enqueueSnackbar} from "notistack"; -import {PlaylistRequest, PlaylistRequestType} from "../../model/playlist-request"; - -const SOURCE_SELF_HOSTED = "self_hosted" -const SOURCE_PROVIDER = "provider" -const SOURCE_CUSTOM = "custom" - -const SOURCE_TYPE_XC = "xtream"; -const SOURCE_TYPE_M3U = "m3u"; - -const SOURCE_TABS = [ - {label: "LABEL.SELF_HOSTED", key: SOURCE_SELF_HOSTED}, - {label: "LABEL.PROVIDER", key: SOURCE_PROVIDER}, - {label: "LABEL.CUSTOM", key: SOURCE_CUSTOM}, -]; - -const SOURCE_TYPES = [ - {label: "LABEL.XTREAM_CODES", value: SOURCE_TYPE_XC}, - {label: "LABEL.M3U", value: SOURCE_TYPE_M3U}, -]; - -const XC_COLUMNS = [ - {name: 'username', label: 'LABEL.USERNAME', fieldType: FormFieldType.TEXT}, - {name: 'password', label: 'LABEL.PASSWORD', fieldType: FormFieldType.TEXT}, - {name: 'url', label: 'LABEL.HOST', fieldType: FormFieldType.TEXT}, -]; - -const M3U_COLUMNS = [ - {name: 'url', label: 'LABEL.URL', fieldType: FormFieldType.TEXT}, -]; - -interface SourceSelectorProps { - serverConfig: ServerConfig; - onSubmit: (req: PlaylistRequest) => boolean; -} - -export interface ISourceSelectorEditor { - open: () => void; - close: () => void; -} - -const SourceSelectorEditor = forwardRef((props: SourceSelectorProps, ref: any) => { - const {serverConfig, onSubmit} = props; - const dialogRef = useRef(null); - const dataRef = useRef({}); - const [inputs, setInputs] = useState([]); - const [targets, setTargets] = useState([]); - const [activeTab, setActiveTab] = useState(SOURCE_SELF_HOSTED); - const [sourceType, setSourceType] = useState(SOURCE_TYPE_XC); - const sourceRef = useRef({input: undefined, target: undefined}); - const translate = useTranslator(); - - const open = useCallback(() => { - dialogRef.current?.showModal(); - }, []); - - const close = useCallback(() => { - dialogRef.current?.close(); - }, []); - - const reference = useMemo(() => ({open, close}), [open, close]); - - useImperativeHandle(ref, () => reference); - - const handleSubmit = useCallback(() => { - let result = undefined; - if (activeTab === SOURCE_SELF_HOSTED) { - // eslint-disable-next-line eqeqeq - if (sourceRef.current.target != null) { - result = {rtype: PlaylistRequestType.TARGET, sourceId: sourceRef.current.target, sourceName: targets.find(i => i.id === sourceRef.current.target)?.name}; - } - } else if (activeTab === SOURCE_PROVIDER) { - // eslint-disable-next-line eqeqeq - if (sourceRef.current.input != null) { - result = {rtype: PlaylistRequestType.INPUT, sourceId: sourceRef.current.input, sourceName: inputs.find(i => i.id === sourceRef.current.input)?.name}; - } - } else { - let record : any = dataRef.current; - if (sourceType === SOURCE_TYPE_XC) { - let username = record.username?.trim() - let password = record.password?.trim() - let url = record.url?.trim() - if (username && password && url) { - result = {rtype: PlaylistRequestType.XTREAM, username, password, url}; - } - } else { - let url = record.url?.trim() - if (url) { - result = {rtype: PlaylistRequestType.M3U, url}; - } - } - } - if (!result) { - enqueueSnackbar(translate('MESSAGES.SOURCE_SELECTOR.MISSING_SELECTION'), {variant: 'error'}); - } else { - if (onSubmit(result)) { - dialogRef.current.close(); - } - } - }, [onSubmit, activeTab, inputs, targets, sourceType, translate]); - - useEffect(() => { - if (serverConfig) { - setInputs(serverConfig.sources?.flatMap(source => source.inputs) ?? []); - setTargets(serverConfig.sources?.flatMap(source => source.targets) ?? []); - } - return noop; - }, [serverConfig]); - - - const handleSourceSelect = useCallback((field: string, value: any) => { - setTimeout(() => { - if (field === 'source_type') { - setSourceType(value) - } else { - sourceRef.current = {...sourceRef.current, [field]: value}; - } - }, 0); - }, []); - - return -
-
- -
- ({value: target.id, label: target.name}))}> -
-
- ({value: input.id, label: input.name}))}> -
-
- - -
-
-
- - -
-
-
; -}); - -export default SourceSelectorEditor; \ No newline at end of file diff --git a/frontend/src/component/source-selector/source-selector.scss b/frontend/src/component/source-selector/source-selector.scss deleted file mode 100755 index 37d671aa2..000000000 --- a/frontend/src/component/source-selector/source-selector.scss +++ /dev/null @@ -1,10 +0,0 @@ -.source-selector { - display: flex; - flex-flow: row nowrap; - padding: 10px 0 10px 0; - background-color: inherit; - .input-field { - flex: 1 1 auto; - } -} - diff --git a/frontend/src/component/source-selector/source-selector.tsx b/frontend/src/component/source-selector/source-selector.tsx deleted file mode 100755 index af2dea299..000000000 --- a/frontend/src/component/source-selector/source-selector.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, {KeyboardEvent, useCallback, useRef} from "react"; - -import './source-selector.scss'; -import {getIconByName} from "../../icons/icons"; -import ServerConfig from "../../model/server-config"; -import {PlaylistRequest, PlaylistRequestType} from "../../model/playlist-request"; -import InputField from "../input-field/input-field"; -import SourceSelectorEditor, {ISourceSelectorEditor,} from "../source-selector-editor/source-selector-editor"; - -const formatSourceSelection = (req: PlaylistRequest): string => { - switch (req?.rtype) { - case PlaylistRequestType.TARGET: { - return `Target: ${req.sourceName}`; - } - case PlaylistRequestType.INPUT: { - return `Provider: ${req.sourceName}`; - } - case PlaylistRequestType.XTREAM: { - return `${req.url}/player_api.php?username=${req.username}&password=${req.password}`; - } - case PlaylistRequestType.M3U: { - return req.url; - } - default: - return ''; - } -}; - -interface SourceSelectorProps { - serverConfig: ServerConfig; - onDownload: (req: PlaylistRequest) => void; -} - -export default function SourceSelector(props: SourceSelectorProps) { - const editorRef = useRef(undefined); - const textFieldRef = useRef(undefined); - const playlistRequestRef = useRef(undefined); - - const {serverConfig, onDownload} = props; - - const handleDownload = useCallback(() => { - if (playlistRequestRef.current) { - onDownload(playlistRequestRef.current); - } - }, [onDownload]); - - const handleKeyPress = useCallback((event: KeyboardEvent) => { - if (event.key === 'Enter') { - handleDownload(); - } - }, [handleDownload]); - - const openPopup = useCallback((evt: any) => { - editorRef.current.open(); - }, []); - - - const handleEditorSubmit = useCallback((req: PlaylistRequest): boolean => { - textFieldRef.current.value = formatSourceSelection(req); - playlistRequestRef.current = req; - return true; - }, []); - - return
- - - - - - -
-} \ No newline at end of file diff --git a/frontend/src/component/split-panel/split-panel.scss b/frontend/src/component/split-panel/split-panel.scss deleted file mode 100644 index ea66e848d..000000000 --- a/frontend/src/component/split-panel/split-panel.scss +++ /dev/null @@ -1,30 +0,0 @@ -.resizable-container { - display: flex; - flex-direction: column; - height: 100vh; - width: 100%; - overflow: hidden; -} - -.top-pane, -.bottom-pane { - overflow: auto; -} - -.top-pane { -} - -.bottom-pane { - flex: 1; -} - -.divider { - height: 6px; - background-color: #4e545d; - cursor: row-resize; - transition: background-color 0.2s; -} - -.divider:hover { - background-color: #aaa; -} diff --git a/frontend/src/component/split-panel/split-panel.tsx b/frontend/src/component/split-panel/split-panel.tsx deleted file mode 100644 index f86a75d05..000000000 --- a/frontend/src/component/split-panel/split-panel.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, {useState, useRef, useEffect, JSX} from 'react'; -import './split-panel.scss'; - -interface SplitPanelProps { - top: JSX.Element; - bottom: JSX.Element; -} - -export const SplitPanel = (props: SplitPanelProps) => { - const containerRef = useRef(null); - const [topHeight, setTopHeight] = useState(300); - const [isDragging, setIsDragging] = useState(false); - - useEffect(() => { - const handleMouseMove = (e: MouseEvent) => { - if (!isDragging || !containerRef.current) return; - const containerTop = containerRef.current.getBoundingClientRect().top; - const newHeight = e.clientY - containerTop; - setTopHeight(newHeight); - }; - - const stopDragging = () => setIsDragging(false); - - window.addEventListener('mousemove', handleMouseMove); - window.addEventListener('mouseup', stopDragging); - - return () => { - window.removeEventListener('mousemove', handleMouseMove); - window.removeEventListener('mouseup', stopDragging); - }; - }, [isDragging]); - - return ( -
-
- {props.top} -
-
setIsDragging(true)}/> -
- {props.bottom} -
-
- ); -}; diff --git a/frontend/src/component/status-view/status-view.scss b/frontend/src/component/status-view/status-view.scss deleted file mode 100644 index f1e4ea31c..000000000 --- a/frontend/src/component/status-view/status-view.scss +++ /dev/null @@ -1,38 +0,0 @@ -@use '../../scss/common'; - -.status-view { - display: flex; - flex-flow: column; - gap: 12px; - padding: 4px; - - &__content { - display: grid; - grid-template-columns: auto 1fr; - gap: 10px; - } - - &__section-title { - display: flex; - flex-flow: row; - align-items: center; - gap: 20px; - color: var(--title-label-color); - font-weight: bold; - font-size: 1.5rem; - border-bottom: 1px solid var(--border-color); - padding-bottom: 4px; - margin: 8px 0; - - button { - @include common.iconButton(); - } - } - - &__col-label { - color: var(--label-color); - } - &__col-value { - - } -} \ No newline at end of file diff --git a/frontend/src/component/status-view/status-view.tsx b/frontend/src/component/status-view/status-view.tsx deleted file mode 100644 index a8030894a..000000000 --- a/frontend/src/component/status-view/status-view.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import {useServices} from "../../provider/service-provider"; -import useTranslator from "../../hook/use-translator"; -import React, {useCallback, useEffect, useState} from "react"; -import {ServerIpCheck, ServerStatus} from "../../model/server-status"; -import {interval} from "rxjs"; -import {first} from "rxjs/operators"; -import './status-view.scss'; -import {getIconByName} from "../../icons/icons"; - -const REQUEST_INTERVAL = 5000; -const STATUS_COLUMNS = [ - "status", - "version", - "build_time", - "server_time", - "memory", - "cache", - "active_users", - "active_user_connections", - "active_provider_connections"]; - -export default function StatusView() { - const services = useServices(); - const translate = useTranslator(); - const [status, setStatus] = useState(); - const [ipCheck, setIpCheck] = useState(); - - const checkIp = useCallback(() => { - services.status().getIpCheck().pipe(first()).subscribe({ - next: (data: ServerIpCheck) => setIpCheck(data), - error: (err) => { - setIpCheck(undefined); - }, - }); - }, [services]) - - useEffect(() => { - const fetchData = () => services.status().getServerStatus().pipe(first()).subscribe({ - next: (data: any) => setStatus(data), - error: () => { - } - }); - fetchData(); - const sub = interval(REQUEST_INTERVAL).subscribe(fetchData); - checkIp(); - return () => sub.unsubscribe(); - }, [services, checkIp]); - - return
- {ipCheck && <> -
{translate("LABEL.IP")}
-
-
{translate("LABEL.IPv4")}
-
{ipCheck?.ipv4 ?? '?'}
-
{translate("LABEL.IPv6")}
-
{ipCheck?.ipv6 ?? '?'}
-
- } -
{translate("LABEL.STATUS")}
-
- {status && STATUS_COLUMNS.map(col => -
{translate('LABEL.' + col.toUpperCase())}
-
{ - (col === "active_provider_connections") ? JSON.stringify((status as any)?.[col]) : (status as any)?.[col] - }
-
- )} -
-
-} \ No newline at end of file diff --git a/frontend/src/component/svg-icon/svg-icon.scss b/frontend/src/component/svg-icon/svg-icon.scss deleted file mode 100644 index fbd460870..000000000 --- a/frontend/src/component/svg-icon/svg-icon.scss +++ /dev/null @@ -1,11 +0,0 @@ -.svg-icon { - fill: currentColor; - width: 1em; - aspect-ratio: 1 / 1; - display: inline-block; - font-size: 1.5rem; - transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - flex-shrink: 0; - user-select: none; - pointer-events: none; -} diff --git a/frontend/src/component/svg-icon/svg-icon.tsx b/frontend/src/component/svg-icon/svg-icon.tsx deleted file mode 100644 index dd411186b..000000000 --- a/frontend/src/component/svg-icon/svg-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import './svg-icon.scss'; -import * as React from "react"; - -interface SvgIconProps { - name: string; - path: string; -} - -function SvgIcon(props: SvgIconProps): React.JSX.Element { - return ; -} - -SvgIcon.displayName = 'SvgIcon'; -export default SvgIcon; diff --git a/frontend/src/component/tab-set/tab-set.scss b/frontend/src/component/tab-set/tab-set.scss deleted file mode 100644 index 1e75cec8e..000000000 --- a/frontend/src/component/tab-set/tab-set.scss +++ /dev/null @@ -1,44 +0,0 @@ - -.tab-set { - display: flex; - flex-flow: row wrap; - gap: 2px; - background-color: var(--tabset-background-color); - border-bottom: 1px solid var(--tabset-border-color); - border-top-left-radius: var(--border-radius); - border-top-right-radius: var(--border-radius); - - list-style-type: none; - padding: 0; - margin: 0; - - &__tab { - cursor: pointer; - user-select: none; - box-sizing: border-box; - background-color: var(--tab-background-color); - color: var(--tab-color); - padding: 4px 8px; - border-top-left-radius: var(--border-radius); - //border-top-right-radius: var(--border-radius); - - &:active { - transform: scale(0.9); - } - - &:focus, &:hover { - background-color: var(--tab-hover-background-color); - color: var(--tab-hover-color); - } - - &-active { - border-bottom: 3px solid var(--tab-avtive-indicator-color); - background-color: var(--tab-active-background-color); - color: var(--tab-active-color); - } - - &:focus-visible { - outline: none; - } - } -} \ No newline at end of file diff --git a/frontend/src/component/tab-set/tab-set.tsx b/frontend/src/component/tab-set/tab-set.tsx deleted file mode 100644 index 223e4dfd1..000000000 --- a/frontend/src/component/tab-set/tab-set.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, {useCallback, useMemo} from "react"; - -import './tab-set.scss'; -import {genUuid} from "../../utils/uuid"; -import useTranslator from "../../hook/use-translator"; - -export interface TabSetTab { - label: string; - key: string; -} - -interface TabSetProps { - tabs: TabSetTab[]; - active: string; - onTabChange: (key: string) => void; -} - -export default function TabSet(props: TabSetProps) { - const {tabs, active, onTabChange} = props; - const uuid = useMemo(() => genUuid(), []); - const translate = useTranslator(); - - const handleTabClick = useCallback((evt: any) => { - const key = evt.target.dataset.key; - onTabChange(key); - }, [onTabChange]); - - const handleTabPress = useCallback((evt: any) => { - let key = (evt as any).key; - if (key === 'Enter' || key === ' ') { - handleTabClick(evt) - } - const rightArrow = key === 'ArrowRight'; - const leftArrow = key === 'ArrowLeft'; - if (rightArrow || leftArrow) { - const key = evt.target.dataset.key; - let idx = tabs?.findIndex( t => t.key === key); - if (idx >= 0) { - if (leftArrow) { - idx -= 1; - } else if (rightArrow) { - idx += 1; - } - if (idx > tabs.length-1) { - idx = 0; - } else if (idx < 0) { - idx = tabs.length-1; - } - } - let elem = document.getElementById(uuid + '-tab-' + idx) - elem?.focus(); - } - }, [uuid, tabs, handleTabClick]); - - return
    - {tabs?.map((tab, idx) => -
  • {translate(tab.label)}
  • )} -
- -} \ No newline at end of file diff --git a/frontend/src/component/tag-input/tag-input.scss b/frontend/src/component/tag-input/tag-input.scss deleted file mode 100644 index 346c8212d..000000000 --- a/frontend/src/component/tag-input/tag-input.scss +++ /dev/null @@ -1,12 +0,0 @@ -.rti--container { - --rti-bg: inherit !important; - --rti-border: none !important; - --rti-tag: var(--checkbox-checked-background-color) !important; - --rti-tag-color: var(--text-color) !important; - --rti-tag-border: var(--border-light-color) !important; - --rti-tag-remove: var(--text-color) !important; - --rti-tag-remove-hover: var(--tab-active-color) !important; - max-width: 660px; - padding: 0; -} - diff --git a/frontend/src/component/tag-input/tag-input.tsx b/frontend/src/component/tag-input/tag-input.tsx deleted file mode 100644 index 57d0ab649..000000000 --- a/frontend/src/component/tag-input/tag-input.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { TagsInput } from "react-tag-input-component"; -import "./tag-input.scss"; - -interface TagInputProps { - name: string; - values: string[]; - onChange: (name: string, values: string[]) => void; - placeHolder?: string; -} - -export default function TagInput(props: TagInputProps) { - const { name, values, onChange, placeHolder } = props; - - const handleTagsChange = (newTags: string[]) => { - onChange(name, newTags); - }; - - return ( -
- -
- ); -} diff --git a/frontend/src/component/tag-select/tag-select.scss b/frontend/src/component/tag-select/tag-select.scss deleted file mode 100644 index 5f823fd7c..000000000 --- a/frontend/src/component/tag-select/tag-select.scss +++ /dev/null @@ -1,24 +0,0 @@ -.tag-select { - display: flex; - flex-flow: row wrap; - gap: 4px; - padding: 2px; - - &__tag { - background-color: transparent; - color: var(--text-color); - padding: 3px 6px; - border-radius: 4px; - cursor: pointer; - border: 1px solid var(--border-light-color); - - &:hover { - border: 1px solid var(--checkbox-checked-background-color); - } - - &-selected { - background-color: var(--checkbox-checked-background-color); - color: var(--text-color); - } - } -} \ No newline at end of file diff --git a/frontend/src/component/tag-select/tags-select.tsx b/frontend/src/component/tag-select/tags-select.tsx deleted file mode 100644 index 8f95c22a0..000000000 --- a/frontend/src/component/tag-select/tags-select.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import './tag-select.scss'; -import {useCallback, useEffect, useMemo, useState} from "react"; -import {noop} from "rxjs"; -import {genUuid} from "../../utils/uuid"; -import useTranslator from "../../hook/use-translator"; - -interface TagSelectProps { - name: string; - options: { value: any, label: string }[] - defaultValues?: any[]; - onSelect: (name: string, values: any) => void; - multi?: boolean; - radio?: boolean; -} - -export default function TagSelect(props: TagSelectProps) { - const {name, multi, options, defaultValues, onSelect, radio} = props; - const translate = useTranslator(); - const [selected, setSelected] = useState>({}); - const uuid = useMemo(() => genUuid(), []); - - useEffect(() => { - if (defaultValues && options) { - const valueIsArray = Array.isArray(defaultValues); - const selections: Record = {} - for (let i = 0; i < options.length; i++) { - const optionValue = options[i].value; - // eslint-disable-next-line eqeqeq - selections[i] = valueIsArray ? defaultValues.includes(optionValue) : defaultValues == optionValue; - } - setSelected(selections); - } - return noop; - }, [defaultValues, options]); - - const handleTagClick = useCallback((evt: any) => { - const idx = evt.target.dataset.idx; - setSelected((selections) => { - const getSelections = () => { - if (multi) { - selections[idx] = !!!selections[idx]; - return {...selections}; - } - if (radio) { - if (!!selections[idx]) { - return selections; - } else { - return {[idx]: !!!selections[idx]} as any; - } - } - return {[idx]: !!!selections[idx]} as any; - }; - const result = getSelections(); - const newSelections = Object.keys(result)?.map((key: any) => result[key] ? options[key].value : undefined).filter(Boolean); - onSelect(name, multi ? newSelections : (newSelections.length ? newSelections[0] : undefined)); - return result; - }); - }, [multi, radio, onSelect, name, options]); - - return
- {options?.map((o, idx) => - {translate(o.label)} - )} -
-} \ No newline at end of file diff --git a/frontend/src/component/target-update-view/target-update-view.scss b/frontend/src/component/target-update-view/target-update-view.scss deleted file mode 100644 index 5f79e0132..000000000 --- a/frontend/src/component/target-update-view/target-update-view.scss +++ /dev/null @@ -1,20 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - -.target-update { - - @include preferences.prefsRoot(); - - @include common.list(); - - - @include preferences.prefsToolbar(); - - - &__content { - overflow: auto; - display: flex; - flex-flow: column; - padding: 12px; - } -} \ No newline at end of file diff --git a/frontend/src/component/target-update-view/target-update-view.tsx b/frontend/src/component/target-update-view/target-update-view.tsx deleted file mode 100644 index e140a9135..000000000 --- a/frontend/src/component/target-update-view/target-update-view.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, {useCallback, useMemo, useRef} from "react"; -import './target-update-view.scss'; -import ServerConfig from "../../model/server-config"; -import ConfigUtils from "../../utils/config-utils"; -import Checkbox from "../checkbox/checkbox"; -import {useServices} from "../../provider/service-provider"; -import {useSnackbar} from "notistack"; -import useTranslator from "../../hook/use-translator"; - -interface TargetUpdateViewProps { - config: ServerConfig -} - -export default function TargetUpdateView(props: TargetUpdateViewProps) { - const {config} = props; - - const services = useServices(); - const translate = useTranslator(); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const targets = useMemo(() => ConfigUtils.getTargetNames(config), [config]); - const selected = useRef([]); - - const handleSelect = useCallback((target: string,checked: boolean) => { - if (checked) { - selected.current.push(target); - } else { - const idx = selected.current.indexOf(target); - selected.current.splice(idx, 1); - } - }, []); - - const handleUpdate = useCallback((evt: any) => { - services.playlist().update(selected.current).subscribe({ - next: () => enqueueSnackbar(translate('MESSAGES.PLAYLIST_UPDATE.SUCCESS'), {variant: 'success'}), - error: (err) => enqueueSnackbar(translate('MESSAGES.PLAYLIST_UPDATE.FAILED') + err, {variant: 'error'}), - }); - }, [services, enqueueSnackbar, translate]); - - return
-
-
-
-
    - {targets.map(t =>
  • )} -
-
-
-} \ No newline at end of file diff --git a/frontend/src/component/tooltip/tooltip.scss b/frontend/src/component/tooltip/tooltip.scss deleted file mode 100644 index 482fbedf5..000000000 --- a/frontend/src/component/tooltip/tooltip.scss +++ /dev/null @@ -1,14 +0,0 @@ - -.tooltip { - position: absolute; - display: inline-block; - cursor: pointer; - background: black; - color: white; - padding: 5px 10px; - border-radius: 5px; - opacity: 0; - transition: opacity 2s ease-in-out; - z-index: 9999; - pointer-events: none; -} diff --git a/frontend/src/component/tooltip/tooltip.tsx b/frontend/src/component/tooltip/tooltip.tsx deleted file mode 100644 index 244fff058..000000000 --- a/frontend/src/component/tooltip/tooltip.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import {useLayoutEffect, useRef} from "react"; -import './tooltip.scss'; -import useTranslator from "../../hook/use-translator"; - -export default function Tooltip() { - - const ref = useRef(null); - const translate = useTranslator(); - - useLayoutEffect(() => { - const handler = (event: any) => { - const tooltip = event.target.dataset.tooltip; - if (tooltip) { - ref.current.innerText = translate(tooltip); - setTimeout(() => { - const el = event.target; - const rect = el.getBoundingClientRect(); - const screenWidth = window.innerWidth; - const screenHeight = window.innerHeight; - - let tooltipX = rect.left; - let tooltipY = rect.top + rect.height + 10; - - ref.current.style.left = tooltipX + 'px'; - ref.current.style.top = tooltipY + 'px'; - - if (tooltipX + ref.current.offsetWidth > screenWidth) { - ref.current.style.left = (screenWidth - ref.current.offsetWidth) + 'px'; - } - - if (tooltipX < 5) { - ref.current.style.left = '5px'; - } - - if (tooltipY < 5) { - ref.current.style.top = '5px'; - } - - if (tooltipY + ref.current.offsetHeight > screenHeight) { - ref.current.style.top = (rect.top - ref.current.offsetHeight - 5) + 'px'; - } - - ref.current.style.opacity = '1'; - }, 0); - } - }; - const handlerLeave = (event: any) => { - ref.current.innerText = ''; - ref.current.style.top = '-1000px'; - ref.current.style.opacity = '0'; - }; - document.addEventListener("mouseover", handler); - document.addEventListener("mouseout", handlerLeave); - return () => { - document.removeEventListener("mouseover", handler); - document.removeEventListener("mouseout", handlerLeave); - } - }, [translate]) - - return
-} diff --git a/frontend/src/component/user-editor/user-editor.scss b/frontend/src/component/user-editor/user-editor.scss deleted file mode 100644 index 81b23e425..000000000 --- a/frontend/src/component/user-editor/user-editor.scss +++ /dev/null @@ -1,23 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - -.tag-select { - min-width: 160px; -} - -.user-editor { - & { - min-height: 400px; - @include preferences.prefsToolbar(); - - } - - &__content { - @media (max-width: common.$media-size-mobile) { - .form-view__col-label { - justify-content: flex-start; - } - } - } -} - diff --git a/frontend/src/component/user-editor/user-editor.tsx b/frontend/src/component/user-editor/user-editor.tsx deleted file mode 100644 index 92b9fb762..000000000 --- a/frontend/src/component/user-editor/user-editor.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, {forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from "react"; -import './user-editor.scss'; -import {Credentials} from "../../model/server-config"; -import useTranslator from "../../hook/use-translator"; -import FormView, {FormFieldType} from "../form-view/from-view"; - -export const STATUS_OPTIONS = [ - {value: 'Active', label: 'Active'}, - {value: 'Expired', label: 'Expired'}, - {value: 'Banned', label: 'Banned'}, - {value: 'Trial', label: 'Trial'}, - {value: 'Disabled', label: 'Disabled'}, - {value: 'Pending', label: 'Pending'}, -]; - -const COLUMNS = [ - {name: 'username', label: 'LABEL.USERNAME', fieldType: FormFieldType.TEXT}, - {name: 'password', label: 'LABEL.PASSWORD', fieldType: FormFieldType.TEXT}, - {name: 'token', label: 'LABEL.TOKEN', fieldType: FormFieldType.TEXT}, - {name: 'server', label: 'LABEL.SERVER', fieldType: FormFieldType.SINGLE_SELECT}, - {name: 'proxy', label: 'LABEL.PROXY', fieldType: FormFieldType.PROXY_TYPE}, - {name: 'max_connections', label: 'LABEL.MAX_CON', fieldType: FormFieldType.NUMBER}, - {name: 'status', label: 'LABEL.STATUS', fieldType: FormFieldType.SINGLE_SELECT, options: STATUS_OPTIONS}, - {name: 'exp_date', label: 'LABEL.EXP_DATE', fieldType: FormFieldType.DATE}, - {name: 'ui_enabled', label: 'LABEL.UI_ENABLED', fieldType: FormFieldType.CHECK}, - {name: 'comment', label: 'LABEL.NOTES', fieldType: FormFieldType.TEXT} -]; - -interface UserViewProps { - serverOptions: { value: string, label: string }[]; - onSubmit: (user: Credentials, target: string) => boolean; -} - -export interface IUserEditor { - edit: (user: Credentials, target: string) => void; - close: () => void; -} - -const UserEditor = forwardRef((props: UserViewProps, ref: any) => { - const {serverOptions, onSubmit} = props; - const dialogRef = useRef(null); - const translate = useTranslator(); - const formFields = useMemo(() => COLUMNS - .map((c) => { - let options = c.options; - if (c.name === 'server') { - options = serverOptions; - } - return ({...c, options}); - }), [serverOptions]); - - const [user, setUser] = useState(undefined); - const [target, setTarget] = useState(undefined); - - const edit = useCallback((user: Credentials, target_name: string) => { - setUser(user); - setTarget(target_name); - dialogRef.current?.showModal(); - }, []); - - const close = useCallback(() => { - dialogRef.current?.close(); - }, []); - - const reference = useMemo(() => ({ edit, close }), [edit, close]); - - useImperativeHandle(ref, () => reference); - - const handleSubmit = useCallback(() => { - if (onSubmit(user, target)) { - dialogRef.current.close(); - } - }, [onSubmit, user, target]); - - return -
-
- -
-
- - -
-
-
; -}); - -export default UserEditor; \ No newline at end of file diff --git a/frontend/src/component/user-playlist/user-playlist.scss b/frontend/src/component/user-playlist/user-playlist.scss deleted file mode 100755 index a3d1d7db8..000000000 --- a/frontend/src/component/user-playlist/user-playlist.scss +++ /dev/null @@ -1,32 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - -.user-playlist { - @include common.container_vert(); - overflow: hidden; - - @include preferences.prefsToolbar(); - - &__content { - @include common.container_horz_no_padding(); - overflow: hidden; - - &-toolbar { - @include common.container_vert_no_padding(); - button { - @include common.iconButton(); - height: 48px; - } - - .button-active { - @include common.activeOptionButton(); - } - } - - &-panels { - @include common.container_vert_no_padding(); - flex: 1 1 auto; - overflow: hidden; - } - } -} \ No newline at end of file diff --git a/frontend/src/component/user-playlist/user-playlist.tsx b/frontend/src/component/user-playlist/user-playlist.tsx deleted file mode 100755 index 69a8bfa8a..000000000 --- a/frontend/src/component/user-playlist/user-playlist.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import './user-playlist.scss'; -import {useServices} from "../../provider/service-provider"; -import {finalize, first, zip} from 'rxjs'; -import {UserPlaylistCategories, UserPlaylistTargetCategories} from "../../model/playlist"; -import LoadingIndicator from '../loading-indicator/loading-indicator'; -import useTranslator from "../../hook/use-translator"; -import UserTargetPlaylist, {BouquetSelection} from "../user-target-playlist/user-target-playlist"; -import {useSnackbar} from "notistack"; - -const TARGET_TABS = [ - {label: "LABEL.XC", key: "xtream"}, - {label: "LABEL.M3U", key: "m3u"}, -]; - -function isEmpty(value: any): boolean { - if (value == null) return true; - if (typeof value === "string") return value.trim() === ""; - if (Array.isArray(value)) return value.length === 0; - if (typeof value === "object") return Object.keys(value).length === 0; - return false; -} - -/* eslint-disable @typescript-eslint/no-empty-interface */ -interface UserPlaylistProps { - -} - -export default function UserPlaylist(props: UserPlaylistProps) { - const services = useServices(); - const translate = useTranslator(); - const [loading, setLoading] = useState(false); - const [categories, setCategories] = useState(undefined); - const [bouquets, setBouquets] = useState(undefined); - const [activeTab, setActiveTab] = useState(TARGET_TABS[0].key); - const selectionRef = useRef<{ xtream: BouquetSelection, m3u: BouquetSelection }>({} as any); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - - useEffect(() => { - setLoading(true); - zip(services.userConfig().getPlaylistBouquet().pipe(first()), - services.userConfig().getPlaylistCategories().pipe(first())).pipe(finalize(() => setLoading(false))) - .subscribe(([bouquet, categories]: [UserPlaylistCategories, UserPlaylistCategories]) => { - if (isEmpty(bouquet)) { - bouquet = undefined; - } - if (isEmpty(categories)) { - categories = undefined; - } - setCategories(categories); - setBouquets(bouquet); - - const mapToSelection = (userBouquet: string[]) => { - if (userBouquet) { - return userBouquet.reduce((acc: any, e: string) => { - acc[e] = true; - return acc; - }, {}) - } - return undefined; - } - const mapTargetToSelection = (targetBouquet: UserPlaylistTargetCategories) => { - return { - live: mapToSelection(targetBouquet?.live), - vod: mapToSelection(targetBouquet?.vod), - series: mapToSelection(targetBouquet?.series), - } - } - - selectionRef.current = { - xtream: mapTargetToSelection(bouquet?.xtream), - m3u: mapTargetToSelection(bouquet?.m3u), - }; - }); - }, [services]); - - const handleActiveTabChange = useCallback((event: any) => { - const tab = event.target.dataset.tab; - setActiveTab(tab); - }, []); - - const handleSave = useCallback(() => { - setLoading(true); - - const toClusterCategories = (clusterBouquet: any, clusterCategories: any): string[] => { - const result = clusterBouquet ? Object.keys(clusterBouquet).filter(key => clusterBouquet[key]) : undefined; - if (result?.length === clusterCategories?.length) { - return undefined; - } - return result; - } - const toTargetCategories = (bs: BouquetSelection, targetCategories: UserPlaylistTargetCategories): UserPlaylistTargetCategories => ({ - live: toClusterCategories(bs?.live, targetCategories?.live), - vod: toClusterCategories(bs?.vod, targetCategories?.vod), - series: toClusterCategories(bs?.series, targetCategories?.series) - }); - - const bouquet: UserPlaylistCategories = { - xtream: toTargetCategories(selectionRef.current.xtream, categories.xtream), - m3u: toTargetCategories(selectionRef.current.m3u, categories.m3u) - } - - services.userConfig().savePlaylistBouquet(bouquet).pipe(first(), finalize(() => setLoading(false))).subscribe({ - next: () => { - enqueueSnackbar(translate('MESSAGES.SAVE.BOUQUET.SUCCESS'), {variant: 'success'}) - }, - error: () => { - enqueueSnackbar(translate('MESSAGES.SAVE.BOUQUET.FAIL'), {variant: 'error'}) - } - }) - }, [services, translate, enqueueSnackbar, categories?.m3u, categories?.xtream]); - - const handleSelectionChange = (selections: BouquetSelection) => { - (selectionRef.current as any)[activeTab] = selections; - } - - return <> - -
-
- - -
-
-
- {TARGET_TABS.map((t) => - )} -
-
- {TARGET_TABS.map((t) => - )} -
-
-
- ; -} diff --git a/frontend/src/component/user-target-playlist/user-target-playlist.scss b/frontend/src/component/user-target-playlist/user-target-playlist.scss deleted file mode 100755 index ecf9b31aa..000000000 --- a/frontend/src/component/user-target-playlist/user-target-playlist.scss +++ /dev/null @@ -1,91 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - -.user-target-playlist { - @include common.container_vert(); - overflow: hidden; - - @include preferences.prefsToolbar(); - - &__header { - font-size: 2rem; - color: var(--label-color); - } - &__categories-panel { - @include common.container_vert_no_padding(); - overflow: hidden; - } - - &__categories-toolbar { - @include common.container_horz_no_padding(); - } - - &__categories { - display: grid; - grid-gap: 10px; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - overflow-x: hidden; - overflow-y: auto; - gap: 8px; - padding: 4px; - - &__toolbar { - display: flex; - flex-flow: row nowrap; - gap: 4px; - flex: 0 0 auto; - padding: 4px; - background-color: var(--background-color); - align-items: center; - - @media (max-width: common.$media-size-mobile) { - flex-wrap: wrap-reverse; - gap: 8px; - } - - &-filter { - display: flex; - flex: 1 1 auto; - background-color: inherit; - .playlist-filter { - input { - font-size: 1em; - } - } - } - - button { - @include common.iconButton(); - height: 48px; - } - - .button-active { - @include common.activeOptionButton(); - text-orientation: upright; - } - } - - &__category { - border:1px solid var(--border-color); - padding: 12px 8px; - border-radius: var(--border-radius); - background-color: var(--card-background-color); - box-sizing: border-box; - display: flex; - flex-flow: row nowrap; - gap: 8px; - white-space: nowrap; - - .checkbox-container { - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - } - - .checkbox-container:hover { - cursor: pointer; - color: var(--label-color); - } - } - } -} \ No newline at end of file diff --git a/frontend/src/component/user-target-playlist/user-target-playlist.tsx b/frontend/src/component/user-target-playlist/user-target-playlist.tsx deleted file mode 100755 index 1e55dc459..000000000 --- a/frontend/src/component/user-target-playlist/user-target-playlist.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, {useCallback, useEffect, useState} from 'react'; -import './user-target-playlist.scss'; -import {UserPlaylistCategories, UserPlaylistTargetCategories} from "../../model/playlist"; -import TabSet from "../tab-set/tab-set"; -import Checkbox from "../checkbox/checkbox"; -import {getIconByName} from "../../icons/icons"; -import PlaylistFilter from "../playlist-filter/playlist-filter"; -import useTranslator from "../../hook/use-translator"; - -const CATEGORY_TABS = [ - {label: "LABEL.LIVE", key: "live"}, - {label: "LABEL.VOD", key: "vod"}, - {label: "LABEL.SERIES", key: "series"} -]; - -export interface BouquetSelection { - live: Record, - vod: Record, - series: Record, -} - -const selectEntries = (selected: boolean, list: string[]): Record => { - return list?.reduce((acc: Record, category: string) => { - acc[category] = selected; - return acc; - }, {}); -} - -interface UserTargetPlaylistProps { - visible: boolean; - categories: UserPlaylistTargetCategories; - bouquet: UserPlaylistTargetCategories; - onSelectionChange: (selections: BouquetSelection) => void, -} - -export default function UserTargetPlaylist(props: UserTargetPlaylistProps) { - const {categories, bouquet, visible, onSelectionChange} = props; - const translate = useTranslator(); - const [filteredCategories, setFilteredCategories] = useState({} as any); - const [selections, setSelections] = useState({} as any); - const [activeTab, setActiveTab] = useState(CATEGORY_TABS[0].key); - const [showSelected, setShowSelected] = useState(false); - - useEffect(() => { - if (categories) { - Object.values(categories).forEach((list: string[]) => { - list.sort((a, b) => a.localeCompare(b, {sensitivity: 'base'} as any)) - }); - const mapToSelection = (userBouquet: any, cluster: string) => { - let clusterBouquet = userBouquet?.[cluster]; - return (categories as any)[cluster].reduce((acc: any, e: string) => { - acc[e] = clusterBouquet ? clusterBouquet?.indexOf(e) >= 0 : true - return acc; - }, {}) - } - const user_bouquets = { - live: mapToSelection(bouquet, 'live'), - vod: mapToSelection(bouquet, 'vod'), - series: mapToSelection(bouquet,'series'), - }; - setSelections(user_bouquets); - } - }, [bouquet, categories]) - - const getActiveCategories = useCallback((key: string) => { - let active = ((filteredCategories as any)?.[key] ?? (categories as any)?.[key]) as any; - if (showSelected) { - return active.filter((c: string) => (selections as any)[key]?.[c]) - } - return active; - }, - [categories, filteredCategories, showSelected, selections]); - - - const handleCheckboxChange = useCallback((value: string, checked: boolean) => { - let clusterSelections = (selections as any)[activeTab] as any; - if (!clusterSelections) { - clusterSelections = {}; - (selections as any)[activeTab] = clusterSelections as any; - } - clusterSelections[value] = checked; - let newSelections = ({...selections, [activeTab]: clusterSelections}); - onSelectionChange(newSelections) - setSelections(newSelections); - }, [activeTab, selections, onSelectionChange]); - - const renderCat = useCallback((cat: string) => { - const clusterSelections = (selections as any)[activeTab]; - // eslint-disable-next-line eqeqeq - const selected = clusterSelections?.[cat] === true; - return
- -
; - }, [handleCheckboxChange, selections, activeTab]); - - const toggleSelection = (selected: boolean) => { - let activeCategories = getActiveCategories(activeTab); - if (activeCategories?.length) { - const newSelections: any = {...selections, [activeTab]: selectEntries(selected, activeCategories)}; - onSelectionChange(newSelections); - setSelections(newSelections); - } - }; - - const handleFilter = useCallback((filter: string, regexp: boolean): void => { - let filter_value = regexp ? filter : filter.toLowerCase(); - if (filter_value?.length) { - const filtered = (categories as any)?.[activeTab]?.filter((cat: string) => { - if (regexp) { - // eslint-disable-next-line eqeqeq - return cat.trim().match(filter_value) != undefined; - } else { - return (cat.trim().toLowerCase().indexOf(filter_value) > -1); - } - }) ?? []; - setFilteredCategories(filteredCategories => ({...filteredCategories, [activeTab]: filtered})); - } else { - setFilteredCategories(filteredCategories => ({ - ...filteredCategories, - [activeTab]: (categories as any)?.[activeTab] - })); - } - }, [activeTab, categories]); - - const handleShowSelected = useCallback((event: any) => { - event.target.blur(); - setShowSelected(!showSelected); - }, [showSelected]); - - const tabs = CATEGORY_TABS.filter(tab => (categories as any)?.[tab.key]?.length); - if (tabs.length === 0) { - return
- {translate("MESSAGES.NO_CONTENT")} -
- } - return <> -
- - {tabs.map(tab =>
-
-
- -
- - - -
-
- {getActiveCategories(tab.key)?.map(renderCat)} -
-
) - } -
- ; -} diff --git a/frontend/src/component/user-view/user-view.scss b/frontend/src/component/user-view/user-view.scss deleted file mode 100644 index a04e40623..000000000 --- a/frontend/src/component/user-view/user-view.scss +++ /dev/null @@ -1,280 +0,0 @@ -@use '../../scss/common'; -@use '../../scss/preferences'; - -dialog:not([open]) { - display: none; -} - -.comment-dialog { - display: flex; - flex-flow: column; - color: var(--text-color); - justify-content: space-between; - - button { - @include common.textButton(); - } - min-height: 200px; - min-width: 280px; -} - -.label { - color: var(--label-color); -} - -.proxy-type { - &-reverse { - } - &-redirect { - color: #fffa00; - } -} - -.status { - &-active, .status-trial { - color: var(--ok-color); - } - &-banned { - color: var(--attention-color); - } - &-expired, &-disabled, &-pending { - color: var(--warn-color); - } -} - -.tag-select { - min-width: 160px; -} - -.checkbox-checked { - color: var(--ok-color); -} - -.checkbox-unchecked { - color: var(--text-color); -} - - -.user { - & { - min-height: 400px; - background-color: inherit; - @include preferences.prefsRoot(); - @include preferences.prefsToolbar(); - } - - &__content { - & { - @include common.container_vert_no_padding(flex-start); - overflow: hidden; - } - label { - font-weight: bold; - color: var(--label-color); - span.target-not-exists { - color: var(--attention-color); - } - } - - &-targets { - display: flex; - flex-flow: column; - flex: 1 1 auto; - gap: 20px; - justify-content: flex-start; - padding-right: 6px; - overflow: hidden; - } - } - - &__target { - & { - display: flex; - flex-flow: column; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - overflow: hidden; - background-color: var(--background-color); - gap: 12px; - } - - &-target { - & { - font-size: 1.2rem; - align-items: center; - display: flex; - flex-flow: row; - padding: 8px; - gap: 12px; - span { - color: var(--label-color); - } - } - - label { - flex: 1 1 0; - display: flex; - flex-flow: row; - align-items: center; - align-content: center; - gap: 8px; - } - - &-options { - font-size: 0.9rem; - color: var(--warn-color); - } - - &-toolbar { - & { - flex: 0 0 auto; - padding-right: 8px; - } - button { - @include common.iconButton(); - } - } - } - - &-user { - &-table-container { - flex: 1 1 auto; - overflow-y: scroll; - background-color: var(--card-background-color); - padding: 8px; - margin: 10px; - box-sizing: border-box; - } - &-table { - display: table; - table-layout: auto; - width: 100%; - height: auto; - box-sizing: border-box; - border-collapse: collapse; - } - - &-row { - display: table-row; - width: auto; - height: 3rem; - clear: both; - border: 1px solid var(--table-border-color); - } - - &-col-header { - padding: 8px; - background-color: var(--background-color); - &-tools { - width: 64px !important; - } - } - - &-col { - & { - display: table-cell; - width: auto; - vertical-align: middle; - padding-left: 4px; - padding-right: 4px; - white-space: nowrap; - - } - - &-value { - display: flex; - flex-flow: row nowrap; - align-items: center; - gap: 4px; - } - - &-action { - cursor: pointer; - span { - pointer-events: none; - } - &:hover { - color: var(--icon-button-hover-background-color); - } - } - - &-comment { - max-width: 200px; - overflow: hidden; - text-overflow: ellipsis; - } - - .visibility { - opacity: 0.5; - cursor: pointer; - &:hover { - color: var(--icon-button-hover-background-color); - } - } - - &-toolbar { - & { - display: flex; - flex-flow: row; - align-items: flex-end; - justify-content: flex-start; - padding-left: 4px; - } - span { - //padding: 4px; - //transform: scale(0.9) translateY(5px); - padding: 4px; - @include common.iconButton(); - &:first-child { - border-radius: var(--icon-button-border-radius) 0 0 var(--icon-button-border-radius) !important; - } - - &:last-child { - border-radius: 0 var(--icon-button-border-radius) var(--icon-button-border-radius) 0 !important; - } - } - } - } - - &-col-label { - display: none; - } - - @media (max-width: 980px) { - &-table { - display: flex; - flex-flow: column; - gap: 20px; - - &-header { - display: none !important; - } - - } - - &-row { - display: flex; - flex-flow: column; - gap: 4px; - height: auto; - } - - &-col-label { - display: block; - padding: 4px; - } - - &-col { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4px; - - &-toolbar { - padding-bottom: 4px; - } - } - } - } - } -} - diff --git a/frontend/src/component/user-view/user-view.tsx b/frontend/src/component/user-view/user-view.tsx deleted file mode 100644 index 6267117f0..000000000 --- a/frontend/src/component/user-view/user-view.tsx +++ /dev/null @@ -1,504 +0,0 @@ -import React, {JSX, useCallback, useEffect, useRef, useState} from "react"; -import './user-view.scss'; -import ServerConfig, {Credentials, TargetUser} from "../../model/server-config"; -import {getIconByName} from "../../icons/icons"; -import {useSnackbar} from "notistack"; -import {useServices} from "../../provider/service-provider"; -import ConfigUtils from "../../utils/config-utils"; -import TabSet, {TabSetTab} from "../tab-set/tab-set"; -import useTranslator from "../../hook/use-translator"; -import DateUtils from "../../utils/date-utils"; -import PlaylistFilter from "../playlist-filter/playlist-filter"; -import UserEditor, {STATUS_OPTIONS} from "../user-editor/user-editor"; -import TextGenerator from "../../utils/text-generator"; - -const renderExpDate = (value: any, hidden?: boolean) => { - if (!value) { - return getIconByName('Unlimited'); - } - return DateUtils.formatDate(value) -}; - -const renderBool = (value: any, hidden?: boolean) => { - return {getIconByName(value ? 'CheckMark' : 'Clear')}; -}; - -const renderComment = (value: any, hidden?: boolean) => { - if (value?.length) { - return {value}; - } - return undefined; -}; - -const renderMaxCon = (value: any, hidden?: boolean) => { - if (!value) { - return getIconByName('Unlimited'); - } - return value; -}; - -const renderStatus = (value: any, hidden?: boolean) => { - if (value) { - return {value} - } - return value; -}; - -const renderProxyType = (value: any, hidden?: boolean) => { - if (value) { - return {value} - } - return value; -}; - -const COLUMNS = [ - {field: 'username', label: 'LABEL.USERNAME'}, - {field: 'password', label: 'LABEL.PASSWORD', hidden: true}, - {field: 'token', label: 'LABEL.TOKEN', hidden: true}, - {field: 'server', label: 'LABEL.SERVER'}, - {field: 'proxy', label: 'LABEL.PROXY', render: renderProxyType}, - {field: 'max_connections', label: 'LABEL.MAX_CON', render: renderMaxCon}, - {field: 'status', label: 'LABEL.STATUS', render: renderStatus}, - {field: 'exp_date', label: 'LABEL.EXP_DATE', render: renderExpDate}, - {field: 'ui_enabled', label: 'LABEL.UI_ENABLED', render: renderBool}, - {field: 'comment', label: 'LABEL.NOTES', render: renderComment, action: true}, -] - -COLUMNS.forEach(col => { - if (!col.render) { - col.render = (value: any, hidden?: boolean) => value ? (hidden ? '***' : '' + value) : ''; - } -}) - -const prepareCredentials = (targetUser: TargetUser[]) => { - targetUser.forEach((user) => { - user.credentials.forEach((credential) => { - if (credential.exp_date) { - credential.exp_date = DateUtils.unixSecondsToDate(credential.exp_date); - } - if (credential.created_at) { - credential.created_at = DateUtils.unixSecondsToDate(credential.created_at); - } - }) - }); -} - -const prepareTargetUserForSave = (targetUser: TargetUser[]): TargetUser[] => { - return targetUser.map((user) => { - let storeUser = {...user, credentials: user.credentials.map(c => ({...c}))}; - storeUser.credentials.forEach((credential) => { - credential.exp_date = DateUtils.toUnixSeconds(credential.exp_date); - credential.created_at = DateUtils.toUnixSeconds(credential.created_at); - }); - return storeUser; - }); -} - -const usernameExists = (uname: string, targets: TargetUser[]): boolean => { - for (const target of targets) { - if (target.credentials.find(c => c.username === uname)) { - return true; - } - } - return false; -}; - -const checkuser = (user: Credentials): string | undefined => { - if (!user.username?.trim().length) { - return "MESSAGES.SAVE.USER.USERNAME_REQUIRED"; - } - // eslint-disable-next-line eqeqeq - if (user.max_connections != undefined) { - const max_con = parseInt(user.max_connections as any); - // eslint-disable-next-line eqeqeq - if (isNaN(max_con) || max_con < 0 || (('' + max_con) != user.max_connections as any)) { - return 'MESSAGES.SAVE.USER.MAX_CONNECTIONS_INVALID'; - } else { - if (max_con < 1) { - user.max_connections = undefined; - } else { - user.max_connections = max_con; - } - } - } - return undefined; -} - - -const createNewUser = (targets: TargetUser[]): Credentials => { - let cnt = 0; - let username = TextGenerator.generateUsername().toLowerCase(); - while (usernameExists(username, targets)) { - username = TextGenerator.generateUsername().toLowerCase(); - cnt++; - if (cnt > 1000) { - username = ""; - break; - } - } - const created_at = Date.now(); - const exp_date = new Date(); - exp_date.setFullYear(exp_date.getFullYear() + 1); - return { - username, - password: TextGenerator.generatePassword(), - token: TextGenerator.generatePassword(), - proxy: 'reverse', - created_at, - exp_date: exp_date.getTime(), - max_connections: 1, - status: "Active", - ui_enabled: true, - comment: undefined, - // @ts-ignore - _ref: undefined, // an indicator for new user - }; -} - -interface UserViewProps { - config: ServerConfig; -} - -export default function UserView(props: UserViewProps) { - const {config} = props; - const services = useServices(); - const translate = useTranslator(); - const userEditorRef = useRef(null); - const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const [serverOptions, setServerOptions] = useState<{ value: string, label: string }[]>([]); - const [targets, setTargets] = useState([]); - const [targetOptions, setTargetOptions] = useState>({}); - const [activeTarget, setActiveTarget] = useState(undefined); - const [tabs, setTabs] = useState([]); - const [showHiddenFields, setShowHiddenFields] = useState>({}); - const [filteredUser, setFilteredUser] = useState>({}); - - useEffect(() => { - if (config) { - const serverOptions = config?.api_proxy?.server?.map(serverInfo => ({ - value: serverInfo.name, - label: serverInfo.name - })); - setServerOptions(serverOptions || []); - const target_names = ConfigUtils.getTargetNames(config); - const missing = config?.api_proxy?.user.filter(target => !target_names.includes(target.target)); - const targetOptions = config.sources - ?.flatMap(s => s.targets || []) - ?.reduce((acc: any, t: any) => { - acc[t.name] = t.options || {}; - return acc; - }, {}); - setTargetOptions(targetOptions); - const result: TargetUser[] = target_names?.map(name => ({ - src: true, - target: name, - credentials: config.api_proxy?.user.find(t => t.target === name)?.credentials || [] - } as any)); - prepareCredentials(result); - missing?.forEach(target => { - result.push({src: false, target: target.target, credentials: target.credentials} as any); - }); - const targets: TargetUser [] = result || []; - setTargets(targets); - if (targets?.length) { - setActiveTarget(targets[0].target); - const hiddenFields = COLUMNS.filter(c => c.hidden); - const hidden: any = {}; - targets.forEach(target => target.credentials.forEach(c => { - if (!c.server) { - c.server = serverOptions?.[0]?.value; - } - hiddenFields.forEach(h => { - hidden[c.username + h.field] = true - }); - })); - setShowHiddenFields(hidden); - } - setTabs(targets.map(target => ({key: target.target, label: target.target}))); - } - }, [config]) - - const handleUserAdd = useCallback((evt: any) => { - const target_name = evt.target.dataset.target; - const target = targets.find(target => target.target === target_name); - if (target) { - const user = createNewUser(targets); - userEditorRef.current.edit(user, target_name); - } - }, [targets]); - - const handleUserRemove = useCallback((evt: any) => { - const username = evt.target.dataset.user; - const target_name = evt.target.dataset.target; - const target = targets.find(target => target.target === target_name); - if (target) { - let idx = target.credentials.findIndex(c => c.username === username) - if (idx >= 0) { - target.credentials.splice(idx, 1); - setTargets([...targets]); - } - } - }, [targets]); - - const handleUserEdit = useCallback((evt: any) => { - const username = evt.target.dataset.user; - const target_name = evt.target.dataset.target; - const target = targets.find(target => target.target === target_name); - if (target) { - const user = target.credentials.find(c => c.username === username); - if (user) { - userEditorRef.current.edit({...user, _ref: user.username}, target_name); - } - } - }, [targets]); - - const handleSave = useCallback(() => { - const usernames: any = {}; - const tokens: any = {}; - for (const target of targets) { - for (const user of target.credentials) { - const err = checkuser(user); - if (err) { - enqueueSnackbar(translate(err), {variant: 'error'}); - return; - } - if (usernames[user.username]) { - enqueueSnackbar(translate("MESSAGES.USER.DUPLICATE_USERNAME") + user.username, {variant: 'error'}); - return; - } - usernames[user.username] = true; - if (user.token) { - if (tokens[user.token]) { - enqueueSnackbar(translate("MESSAGES.USER.DUPLICATE_TOKEN") + user.token, {variant: 'error'}); - return; - } - tokens[user.token] = true; - } - } - } - const targetUser = targets.map(t => { - t.credentials.forEach(c => { - c.username = c.username.trim(); - c.password = c.password.trim(); - c.token = c.token?.trim(); - }) - return {target: t.target, credentials: t.credentials} - }); - const toSaveTargetUser = prepareTargetUserForSave(targetUser); - services.config().saveTargetUser(toSaveTargetUser).subscribe({ - next: () => enqueueSnackbar(translate('MESSAGES.SAVE.USER.SUCCESS'), {variant: 'success'}), - error: (err) => enqueueSnackbar(translate('MESSAGES.SAVE.USER.FAIL'), {variant: 'error'}) - }); - }, [targets, services, enqueueSnackbar, translate]); - - const handleVisibility = useCallback((evt: any) => { - let key = evt.target.dataset.hiddenkey; - setShowHiddenFields((hiddenFields: any) => ({...hiddenFields, [key]: !hiddenFields[key]})); - }, []); - - const filterTarget = useCallback((target: TargetUser, filter: string, regexp: boolean, filterForStatus: string): void => { - if (target) { - if (filter?.length) { - let filtered = target.credentials.filter((credential: Credentials) => { - if (regexp) { - // eslint-disable-next-line eqeqeq - return credential.username.trim().match(filter) != undefined; - } else { - return (credential.username.trim().toLowerCase().indexOf(filter) > -1); - } - }) ?? []; - if (filterForStatus) { - filtered = filtered.filter((c: Credentials) => c.status === filterForStatus); - } - setFilteredUser((filteredUser: any) => ({ - ...filteredUser, - [target.target]: {filter, regexp, status: filterForStatus, user: filtered} - })); - } else { - let filtered = undefined; - if (filterForStatus) { - filtered = { - filter: undefined, - regexp: false, - status: filterForStatus, - user: target.credentials.filter((c: Credentials) => c.status === filterForStatus) - }; - } - setFilteredUser((filteredUser: any) => ({ - ...filteredUser, [target.target]: filtered - })); - } - } - }, []); - - const handleFilter = useCallback((filter: string, regexp: boolean): void => { - let filter_value = regexp ? filter : filter?.toLowerCase(); - const target = targets.find(t => t.target === activeTarget); - if (target) { - let currentFilter = filteredUser[target.target]; - filterTarget(target, filter_value, regexp, currentFilter?.status); - } - }, [activeTarget, targets, filterTarget, filteredUser]); - - const handleUserEditorSubmit = useCallback((user: Credentials, target_name: string): boolean => { - const userRef = (user as any)._ref; - const err = checkuser(user); - if (err) { - enqueueSnackbar(translate(err), {variant: 'error'}); - return false; - } - if ((userRef && user.username === userRef) || !usernameExists(user.username, targets)) { - const target = targets.find(target => target.target === target_name); - if (target) { - delete (user as any)._ref; - if (userRef) { - const index = target.credentials.findIndex(c => c.username === userRef); - target.credentials.splice(index, 1, user); - } else { - target.credentials.push(user); - } - setTargets(targets.slice()); - return true; - } else { - enqueueSnackbar(translate("MESSAGES.USER.TARGET_NOT_FOUND") + target_name, {variant: 'error'}); - } - } else { - enqueueSnackbar(translate("MESSAGES.USER.DUPLICATE_USERNAME") + user.username, {variant: 'error'}); - } - return false; - }, [targets, translate, enqueueSnackbar]); - - const renderTargetOptions = useCallback((target: string): JSX.Element => { - let options = targetOptions?.[target]; - if (options?.force_redirect?.length > 2) { - return
{translate('HINT.CONFIG.USER.TARGET_FORCE_REDIRECT')} {options.force_redirect}
; - } - return <>; - }, [targetOptions, translate]); - - const handleStatusFilter = useCallback((evt: any) => { - const target_name = evt.target.dataset.target; - let target = targets.find((t) => t.target === target_name); - if (target) { - let value = evt.target.value; - value = value?.length ? value : undefined; - let currentFilter = filteredUser[target_name]; - filterTarget(target, currentFilter?.filter, !!currentFilter?.regexp, value) - } - }, [filteredUser, filterTarget, targets]); - - const handleColumnAction = useCallback((evt: any) => { - const field = evt.target.dataset.field; - if (field === 'comment') { - const target_name = evt.target.dataset.target; - let target = targets.find((t) => t.target === target_name); - if (target) { - const username = evt.target.dataset.username; - const user = target.credentials.find(usr => usr.username === username); - if (user) { - const dialog: any = document.getElementById("comment-dialog"); - const comment = document.getElementById("comment-text"); - comment.textContent = user.comment; - dialog.showModal(); - } - } - } - }, [targets]); - - return
-
- - -
- -
-
- { - targets?.map(target =>
-
- - -
{translate("LABEL.STATUS")}
- - -
- -
-
- -
-
-
-
- {COLUMNS.map(col =>
-
)} -
- - {(filteredUser[target.target]?.user ?? target.credentials).map((usr, idx) => -
-
-
- - {getIconByName('PersonRemove')} - - - {getIconByName('Edit')} - -
-
- {COLUMNS.map(c =>
-
- -
-
{c.hidden && - - {getIconByName('Visibility')}} - {c.render((usr as any)[c.field], (showHiddenFields[usr.username + c.field] ?? c.hidden))} -
-
) - } -
- )} -
-
-
)} -
-
- - -

- -
-
-} \ No newline at end of file diff --git a/frontend/src/component/video-player/video-player.scss b/frontend/src/component/video-player/video-player.scss deleted file mode 100644 index 2bd02c65b..000000000 --- a/frontend/src/component/video-player/video-player.scss +++ /dev/null @@ -1,13 +0,0 @@ -.video-player { - display: flex; - justify-content: center; - height: 100%; - width: auto; - background-color: black; - - .video-js { - background-color: black; - height: 100%; - width: 100%; - } -} \ No newline at end of file diff --git a/frontend/src/component/video-player/video-player.tsx b/frontend/src/component/video-player/video-player.tsx deleted file mode 100644 index eac131d6a..000000000 --- a/frontend/src/component/video-player/video-player.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import React, {useCallback, useEffect, useRef} from 'react'; -import './video-player.scss'; -import {first, Observable, Subscription} from "rxjs"; -import {PlaylistItem, PlaylistItemType} from "../../model/playlist"; -import videojs from "video.js"; -import 'videojs-mpegtsjs'; -import "video.js/dist/video-js.css"; -import {useServices} from '../../provider/service-provider'; -import {PlaylistRequest, PlaylistRequestType} from "../../model/playlist-request"; - -const VIDEOJS_OPTIONS: any = { - autoplay: true, - controls: true, - responsive: false, - fluid: true, - preload: "none", -} - -const DEFAULT_OPTIONS: any = { - ...VIDEOJS_OPTIONS, - sources: [], - poster: undefined, -}; - -const MPEGTS_OPTIONS: any = { - mediaDataSource: { - type: 'mpegts', - isLive: true, - cors: true, - withCredentials: false, - }, - config: { - enableWorker: true, - enableWorkerForMSE: true, - enableStashBuffer: true, - } -}; - -const HLS_OPTIONS: any = { - html5: { - vhs: { - cors: true, - overrideNative: true - }, - nativeAudioTracks: false, - nativeVideoTracks: false - } -}; - -// @ts-ignore -type Player = videojs.Player | null; - -const getVideoOptions = (playlistItem: PlaylistItem, url: string): { mimeType: string, options: any } => { - switch (playlistItem.item_type) { - case PlaylistItemType.Video: - case PlaylistItemType.Series: - return {mimeType: 'video/mp4', options: {}}; - case PlaylistItemType.SeriesInfo: - return {mimeType: 'application/json', options: {}}; - case PlaylistItemType.Live: - return { - mimeType: 'video/mp2t', options: { - mpegtsjs: { - ...MPEGTS_OPTIONS, - mediaDataSource: {...MPEGTS_OPTIONS.mediaDataSource, url} - } - } - }; - case PlaylistItemType.Catchup: - case PlaylistItemType.LiveUnknown: - case PlaylistItemType.LiveHls: - return {mimeType: 'application/x-mpegURL', options: HLS_OPTIONS}; - } - return {mimeType: 'application/octet-stream', options: {}}; -} - -interface VideoPlayerProps { - channel: Observable<[PlaylistItem, PlaylistRequest]>; - onReady?: (player: any) => void; -} - -export const VideoPlayer = ({channel, onReady}: VideoPlayerProps) => { - const services = useServices(); - const videoRef = useRef(null); - const playerRef = useRef(null); - - const playVideo = useCallback((playlistItem: PlaylistItem, url: string) => { - let options = getVideoOptions(playlistItem, url); - // TODO mimetype - const playerOptions = { - ...DEFAULT_OPTIONS, - sources: [{ - src: url, - type: options.mimeType, - }], - poster: playlistItem.logo ?? playlistItem.logo_small, - ...options.options - }; - - try { - - if (playerRef.current && playerRef.current.currentSrc() !== playerOptions.sources[0].src) { - const player = playerRef.current; - player.options(playerOptions); - player.poster(playerOptions.poster); - player.src(playerOptions.sources); - } - } catch (e) { - console.error(e); - } - }, []) - - const handlePlayVideo = useCallback(([playlistItem, playlistRequest]: [playlistItem: PlaylistItem, playlistRequest: PlaylistRequest]) => { - switch (playlistRequest.rtype) { - case PlaylistRequestType.TARGET: - if (playlistItem.item_type === PlaylistItemType.LiveHls) { - playVideo(playlistItem, playlistItem.url); - } else { - services.playlist().getWebPlayerUrl(playlistItem, playlistRequest).pipe(first()).subscribe({ - next: (url: string) => { - playVideo(playlistItem, url); - }, - error: (error: any) => { - playVideo(playlistItem, playlistItem.url); - }, - }); - } - break; - case PlaylistRequestType.INPUT: - case PlaylistRequestType.XTREAM: - case PlaylistRequestType.M3U: - playVideo(playlistItem, playlistItem.url); - break; - } - }, [services, playVideo]); - - useEffect(() => { - const sub: Subscription = channel.subscribe({ - next: handlePlayVideo, - }); - return () => sub.unsubscribe(); - }, [channel, handlePlayVideo]); - - useEffect(() => { - if (!playerRef.current && videoRef.current) { - const videoElement = document.createElement("video-js"); - videoElement.classList.add("vjs-big-play-centered"); - videoElement.setAttribute('controls', VIDEOJS_OPTIONS.controls ?? 'false'); - videoElement.setAttribute('autoplay', VIDEOJS_OPTIONS.autoplay ?? 'false'); - videoElement.setAttribute('responsive', VIDEOJS_OPTIONS.responsive ?? 'false'); - //videoElement.setAttribute('fluid', VIDEOJS_OPTIONS.fluid ?? 'false'); - videoRef.current.appendChild(videoElement); - - const player = videojs(videoElement, {}, () => { - videojs.log("player is ready"); - onReady && onReady(player); - }); - - playerRef.current = player; - // Error handling - player.on('error', () => { - const error = player.error(); - console.error('Video.js Error:', error); - - if (error && error.code === 4) { - console.warn('Not found.'); - } - }); - - // const mpegtsPlayer = (player.tech(true) as any).flvPlayer; - // if (mpegtsPlayer) { - // mpegtsPlayer.on(mpegts.Events.ERROR, (type, details) => { - // console.error("mpegts.js ERROR", type, details); - // }); - // } - } - }, [onReady]); - - useEffect(() => { - return () => { - if (playerRef.current && !playerRef.current.isDisposed()) { - playerRef.current.dispose(); - playerRef.current = null; - } - }; - }, []); - - return ( -
-
- ); -} - -export default VideoPlayer; diff --git a/frontend/src/config.ts b/frontend/src/config.ts deleted file mode 100644 index d7f6e1690..000000000 --- a/frontend/src/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -const dev = { - app: { - version: process.env.REACT_APP_VERSION, - }, -}; - -const prod = { - app: { - version: process.env.REACT_APP_VERSION, - }, -}; - -const config = process.env.REACT_APP_STAGE === 'production' ? prod : dev; - -const DefaultConfig = { - // Add common config values here - max_attachment_size: 5000000, - ...config -}; - -export default DefaultConfig; diff --git a/webui/src/error.rs b/frontend/src/error.rs similarity index 100% rename from webui/src/error.rs rename to frontend/src/error.rs diff --git a/frontend/src/hook/use-const.ts b/frontend/src/hook/use-const.ts deleted file mode 100644 index 21a2486c8..000000000 --- a/frontend/src/hook/use-const.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {useRef} from "react"; - -type ResultBox = { v: T } - -type ConstantFunction = () => T; - -export default function useConst(fnOrValue: T | ConstantFunction): T { - const ref = useRef>(undefined) - if (!ref.current) { - // @ts-ignore - const value: T = (typeof fnOrValue == 'function') ? fnOrValue() : fnOrValue; - ref.current = { v: value } - } - return ref.current.v -} diff --git a/frontend/src/hook/use-translator.ts b/frontend/src/hook/use-translator.ts deleted file mode 100644 index 775be2fca..000000000 --- a/frontend/src/hook/use-translator.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {useTranslation} from "react-i18next"; -import {useEffect, useMemo} from "react"; -import {ReplaySubject, Subject} from "rxjs"; - -export type TranslateFunc = (key: string, variables?: Record) => string; - -export const LANGUAGE_LOCAL_STORAGE_KEY: string = "language" - -export const LANGUAGE: Subject = new ReplaySubject(1); -export default function useTranslator(): (key: string, variables?: any) => string { - const [t, i18n] = useTranslation('common'); - useEffect(() => { - LANGUAGE.subscribe(lang => { - localStorage.setItem(LANGUAGE_LOCAL_STORAGE_KEY, lang); - i18n.changeLanguage(lang).then(() => {}); - }); - }, [i18n]); - - return useMemo((): TranslateFunc => t, [t]); -} diff --git a/webui/src/hooks/mod.rs b/frontend/src/hooks/mod.rs similarity index 100% rename from webui/src/hooks/mod.rs rename to frontend/src/hooks/mod.rs diff --git a/webui/src/hooks/use_icon_context.rs b/frontend/src/hooks/use_icon_context.rs similarity index 100% rename from webui/src/hooks/use_icon_context.rs rename to frontend/src/hooks/use_icon_context.rs diff --git a/webui/src/hooks/use_server_status.rs b/frontend/src/hooks/use_server_status.rs similarity index 100% rename from webui/src/hooks/use_server_status.rs rename to frontend/src/hooks/use_server_status.rs diff --git a/webui/src/hooks/use_service_context.rs b/frontend/src/hooks/use_service_context.rs similarity index 100% rename from webui/src/hooks/use_service_context.rs rename to frontend/src/hooks/use_service_context.rs diff --git a/frontend/src/icons/icons.tsx b/frontend/src/icons/icons.tsx deleted file mode 100644 index 667c36a05..000000000 --- a/frontend/src/icons/icons.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import createIcon from "../utils/icon-utils"; -import * as React from "react"; - -const ICON: Record = {}; - -export const getIconByName = (name: string): any => { - if (name) { - // @ts-ignore - return ICON[name]; - } - return undefined; -} - -const DEFAULT_ICONS = { - Logo: 'M 14.09194,7.6882684e-4 C 13.973764,9.5800672e-5 13.856738,1.8569152e-4 13.740338,0.00128559 10.134203,0.03533121 7.3722672,0.9073449 5.3120566,2.2058124 2.6199728,3.9025243 1.1166249,6.3549771 0.44196532,8.8193928 4.4918101,7.9934421 7.5738185,8.392102 10.393515,9.4901562 V 3.0538267 c 0,-1.0502871 0.717633,-1.3329492 1.494308,-0.8443969 l 5.939123,3.7362233 C 17.883676,5.8672459 17.940027,5.7886156 17.993602,5.7100075 18.773785,4.4409406 19.517515,2.4795189 20.024457,0.67721647 17.837484,0.22159145 15.864573,0.01086418 14.09194,7.6882684e-4 Z M 12.283121,4.6015429 c -0.352571,-0.00358 -0.610222,0.2693437 -0.610222,0.817525 V 18.544943 c 0,0.716278 0.702783,1.00662 1.134576,0.772566 L 23.366199,12.42229 c 0.321466,-0.209944 0.293977,-0.733308 0,-1.002527 L 12.807475,4.7777605 C 12.623215,4.6618552 12.443381,4.6031688 12.283121,4.6015429 Z M 4.6820187,10.631177 c -1.3670233,0.0065 -2.8875164,0.207303 -4.66075611,0.63924 -0.22262,3.841376 1.29470801,7.532859 4.53424051,9.96688 3.3072703,2.484918 8.4898149,3.647573 15.5090939,1.986968 -0.453335,-1.879096 -1.379185,-3.757389 -2.654289,-5.468942 l -5.522485,3.606515 c -0.568764,0.308297 -1.494308,-0.07453 -1.494308,-1.018031 V 12.036783 C 8.6043863,11.2147 6.6723813,10.706689 4.6820187,10.631177 Z', - ContentCopy: 'M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z', - DeleteSweep: 'M15 16h4v2h-4zm0-8h7v2h-7zm0 4h6v2h-6zM3 18c0 1.1.9 2 2 2h6c1.1 0 2-.9 2-2V8H3v10zM14 5h-3l-1-1H6L5 5H2v2h12z', - Delete: 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z', - Search: 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z', - Clear: 'M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z', - ClearSearch: 'M 10.82534,3 C 7.3479487,3 4.5022347,5.716944 4.2480387,9.1748729 H 6.3014596 C 6.5556707,6.859298 8.4664087,5.0582909 10.82534,5.0582909 c 2.531786,0 4.575514,2.068585 4.575514,4.6311538 0,2.5625733 -2.043728,4.6311543 -4.575514,4.6311543 -0.172856,0 -0.335527,-0.03153 -0.508391,-0.05226 v 2.080402 c 0.172857,0.02062 0.335527,0.03015 0.508391,0.03015 1.637017,0 3.14233,-0.60752 4.30146,-1.616081 l 0.274054,0.289447 v 0.812061 L 20.484758,21 22,19.466331 16.926025,14.320604 h -0.802303 l -0.28597,-0.277387 c 0.996446,-1.173226 1.596663,-2.696845 1.596663,-4.3537672 0,-3.6946315 -2.958832,-6.6894447 -6.609075,-6.6894447 z M 2.7228671,11.048238 2,11.777886 4.5121637,14.320599 2,16.863312 l 0.7228671,0.729648 2.5101775,-2.540703 2.5121641,2.540703 0.720882,-0.729648 -2.510178,-2.542713 2.510178,-2.542713 -0.720882,-0.729648 -2.5121641,2.542713 z', - ExpandMore: 'M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z', - ExpandLess: 'm12 8-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z', - ChevronRight: 'M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z', - LinkRounded: 'M17 7h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c1.65 0 3 1.35 3 3s-1.35 3-3 3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-9 5c0 .55.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1H9c-.55 0-1 .45-1 1zm2 3H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h3c.55 0 1-.45 1-1s-.45-1-1-1H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h3c.55 0 1-.45 1-1s-.45-1-1-1z', - PlayArrow: 'M8 5v14l11-7z', - ArrowLeft: 'm14 7-5 5 5 5V7z', - ArrowRight: 'm10 17 5-5-5-5v10z', - ArrowDown: 'm7 10 5 5 5-5z', - ArrowUp: 'm7 14 5-5 5 5z', - CloudDownload: 'M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z', - Download: 'M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z', - Config: 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z', - PersonAdd: 'M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z', - PersonRemove: 'M14 8c0-2.21-1.79-4-4-4S6 5.79 6 8s1.79 4 4 4 4-1.79 4-4zm3 2v2h6v-2h-6zM2 18v2h16v-2c0-2.66-5.33-4-8-4s-8 1.34-8 4z', - WebSearch: 'M19.3 16.9c.4-.7.7-1.5.7-2.4 0-2.5-2-4.5-4.5-4.5S11 12 11 14.5s2 4.5 4.5 4.5c.9 0 1.7-.3 2.4-.7l3.2 3.2 1.4-1.4-3.2-3.2zm-3.8.1c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5 2.5 1.1 2.5 2.5-1.1 2.5-2.5 2.5zM12 20v2C6.48 22 2 17.52 2 12S6.48 2 12 2c4.84 0 8.87 3.44 9.8 8h-2.07c-.64-2.46-2.4-4.47-4.73-5.41V5c0 1.1-.9 2-2 2h-2v2c0 .55-.45 1-1 1H8v2h2v3H9l-4.79-4.79C4.08 10.79 4 11.38 4 12c0 4.41 3.59 8 8 8z', - CheckMark: 'M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z', - Checked: 'M 4.2222224,2 C 3,2 2,3.000021 2,4.2222362 V 19.777764 C 2,20.999979 3,22 4.2222224,22 H 19.777778 C 20.999989,22 22,20.999979 22,19.777764 V 4.2222362 C 22,3.000021 20.999989,2 19.777778,2 Z m 0,2.2222362 H 19.777778 V 19.777764 H 4.2222224 Z M 17.288622,7.1063517 10.441833,13.953134 6.7113668,10.233533 5.2465223,11.698352 10.441833,16.89369 18.753467,8.5820472 Z', - Hourglass: 'M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6zm10 14.5V20H8v-3.5l4-4 4 4zm-4-5-4-4V4h8v3.5l-4 4z', - Error: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z', - Regexp: 'M 14.219989,2 V 6.6779027 L 10.218308,4.3991767 8.9526346,6.5626756 13.00229,8.8690562 8.8806751,11.216304 l 1.2656739,2.16227 4.07364,-2.3199 v 4.62413 h 2.532593 v -4.677903 l 3.98081,2.26705 L 22,11.108144 18.066228,8.8681345 21.985982,6.6358063 20.720308,4.4735364 16.752582,6.7332118 V 2 Z M 4.8198059,16.438384 C 3.2775726,16.438384 1.9999995,17.698564 2,19.219808 1.9999995,20.741043 3.2775726,22 4.8198059,22 c 1.542234,0 2.8185603,-1.258957 2.8185597,-2.780192 6e-7,-1.521244 -1.2763257,-2.781424 -2.8185597,-2.781424 z', - User: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z', - Refresh: 'M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z', - ApiServer: 'm20.2 5.9.8-.8C19.6 3.7 17.8 3 16 3s-3.6.7-5 2.1l.8.8C13 4.8 14.5 4.2 16 4.2s3 .6 4.2 1.7zm-.9.8c-.9-.9-2.1-1.4-3.3-1.4s-2.4.5-3.3 1.4l.8.8c.7-.7 1.6-1 2.5-1 .9 0 1.8.3 2.5 1l.8-.8zM19 13h-2V9h-2v4H5c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2zM8 18H6v-2h2v2zm3.5 0h-2v-2h2v2zm3.5 0h-2v-2h2v2z', - Warn: 'M 11.999765,2 2,22 h 20 z m 0,4.1999619 6.845654,13.6948891 H 5.1545815 Z m -0.909027,4.2211761 v 5.263416 h 1.818524 v -5.263416 z m 0,6.31599 v 2.105149 h 1.818524 v -2.105149 z', - Gallery: 'M 11.999742,2 C 6.5002947,2 1.9999999,6.5002944 2,11.999742 2,17.499188 6.5002947,22 11.999742,22 17.499187,22 22,17.499188 22,11.999742 22,6.5002944 17.499187,1.9999999 11.999742,2 Z M 7.4796784,5.5010206 H 9.2940081 C 10.454244,5.5013381 11.3944,6.4424302 11.393587,7.6026664 v 1.8143297 c -3.18e-4,1.1594289 -0.94015,2.0992499 -2.0995789,2.0995789 H 7.4796784 C 6.3194422,11.517395 5.3783618,10.577232 5.3780327,9.4169961 V 7.6026664 C 5.3772125,6.4416227 6.3186347,5.5002073 7.4796784,5.5010206 Z m 7.2263136,0 h 1.816396 c 1.160237,3.175e-4 2.100392,0.9414098 2.099579,2.1016458 v 1.8143297 c -3.17e-4,1.1594289 -0.94015,2.0992499 -2.099579,2.0995789 h -1.816396 c -1.159429,-3.18e-4 -2.09925,-0.94015 -2.099579,-2.0995789 V 7.6026664 c -8.2e-4,-1.1602362 0.939342,-2.1013166 2.099579,-2.1016458 z M 7.4796784,12.483425 h 1.8143297 c 1.1594289,3.18e-4 2.0992499,0.94015 2.0995789,2.099579 v 1.816397 c -3.18e-4,1.159428 -0.94015,2.099249 -2.0995789,2.099578 H 7.4796784 C 6.3194422,18.4998 5.3783618,17.559637 5.3780327,16.399401 v -1.816397 c 3.175e-4,-1.160236 0.9414095,-2.100391 2.1016457,-2.099579 z m 7.2263136,0 h 1.816396 c 1.159429,3.18e-4 2.09925,0.94015 2.099579,2.099579 v 1.816397 c -3.17e-4,1.159428 -0.94015,2.099249 -2.099579,2.099578 h -1.816396 c -1.159429,-3.17e-4 -2.09925,-0.94015 -2.099579,-2.099578 v -1.816397 c 3.17e-4,-1.159429 0.94015,-2.09925 2.099579,-2.099579 z', - Editor: 'M8 5v14l11-7z', - ScheduleAdd: 'm 15.427906,2 0.0026,4.3103469 -4.402514,-0.00424 0.0016,2.4026963 4.402514,0.00475 0.0026,4.2565168 2.402815,0.0026 -0.0021,-4.2565172 4.164457,0.00517 -0.0016,-2.403213 -4.164974,-0.00465 -0.0026,-4.3103469 z M 9.2342365,7.5143019 c -3.9841557,5e-7 -7.2343587,3.2535111 -7.2343586,7.2428491 0,3.989335 3.2502029,7.242849 7.2343586,7.242849 3.9841545,0 7.2322915,-3.253514 7.2322915,-7.242849 0,-0.06325 -0.0014,-0.126255 -0.0031,-0.18912 H 14.68171 c 0.0022,0.06278 0.0041,0.125793 0.0041,0.18912 0,3.025716 -2.429816,5.460169 -5.4516058,5.460169 -3.0217902,0 -5.4531561,-2.434453 -5.4531561,-5.460169 0,-3.025718 2.4313659,-5.4586186 5.4531561,-5.4586186 0.1480969,0 0.2937091,0.00764 0.4387121,0.019119 C 9.6657863,8.738416 9.6404593,8.1019918 9.6781163,7.5292876 9.531058,7.5202214 9.3835267,7.5143032 9.2342349,7.5143032 Z M 8.078806,10.651303 v 5.534575 h 5.462975 V 14.71943 H 9.5432468 v -4.068127 z', - ScheduleRemove: 'm 14.051424,1.9998779 -1.742213,1.697054 3.122965,3.0458089 -3.191235,3.1104043 1.741155,1.6975709 3.191765,-3.1098876 3.084332,3.0080846 L 21.999878,9.7518594 18.916075,6.7432575 21.935312,3.8023519 20.194157,2.1042643 17.17439,5.0462036 Z M 9.4085202,7.51427 c -4.0802372,6e-7 -7.4086424,3.253631 -7.4086423,7.242969 0,3.989334 3.3284051,7.242968 7.4086423,7.242968 4.0802358,0 7.4070548,-3.253634 7.4070548,-7.242968 0,-0.06325 -0.0014,-0.126271 -0.0032,-0.189136 h -1.824773 c 0.0023,0.06278 0.0042,0.125808 0.0042,0.189136 0,3.025716 -2.488677,5.460131 -5.5833398,5.460131 -3.0946634,0 -5.5843983,-2.434415 -5.5843983,-5.460131 0,-3.025718 2.4897349,-5.458582 5.5843983,-5.458582 0.1516683,0 0.3008134,0.00764 0.4493133,0.01912 C 9.8504755,8.7385417 9.8245025,8.10196 9.8630655,7.5292556 9.712521,7.5201898 9.5614122,7.51427 9.4085202,7.51427 Z m -1.1833506,3.137276 v 5.534546 H 13.820153 V 14.719515 H 9.7249975 v -4.067969 z', - Logout: 'm17 7-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4z', - SelectAll: 'M 2,9.2105263 H 13.578947 V 11.315789 H 2 Z M 2,5 H 13.578947 V 7.105263 H 2 Z m 0,8.421054 h 7.368421 v 2.105263 H 2 Z m 18.515789,-2.178949 -4.473684,4.46316 -2.231579,-2.23158 -1.48421,1.484211 3.715789,3.726316 L 22,12.726317 Z', - DeselectAll: 'M 2,5 V 7.1538458 H 13.612371 V 5 Z M 2,9.3076922 V 11.461539 H 13.612371 V 9.3076922 Z m 14.082473,1.7899618 -1.265978,1.392431 2.327832,2.559791 -2.327832,2.557693 L 16.082473,19 18.408247,16.442307 20.734022,19 22,17.607569 19.674225,15.049876 22,12.490085 20.734022,11.097654 18.408247,13.655347 Z M 2,13.615385 v 2.153847 h 7.3896896 v -2.153847 z', - Visibility: 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5m0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3', - Edit: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75z', - Calendar: 'M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 16H5V10h14zM9 14H7v-2h2zm4 0h-2v-2h2zm4 0h-2v-2h2zm-8 4H7v-2h2zm4 0h-2v-2h2zm4 0h-2v-2h2z', - Unlimited: 'M18.6 6.62c-1.44 0-2.8.56-3.77 1.53L12 10.66 10.48 12h.01L7.8 14.39c-.64.64-1.49.99-2.4.99-1.87 0-3.39-1.51-3.39-3.38S3.53 8.62 5.4 8.62c.91 0 1.76.35 2.44 1.03l1.13 1 1.51-1.34L9.22 8.2C8.2 7.18 6.84 6.62 5.4 6.62 2.42 6.62 0 9.04 0 12s2.42 5.38 5.4 5.38c1.44 0 2.8-.56 3.77-1.53l2.83-2.5.01.01L13.52 12h-.01l2.69-2.39c.64-.64 1.49-.99 2.4-.99 1.87 0 3.39 1.51 3.39 3.38s-1.52 3.38-3.39 3.38c-.9 0-1.76-.35-2.44-1.03l-1.14-1.01-1.51 1.34 1.27 1.12c1.02 1.01 2.37 1.57 3.82 1.57 2.98 0 5.4-2.41 5.4-5.38s-2.42-5.37-5.4-5.37', - Today: 'M16.53 11.06 15.47 10l-4.88 4.88-2.12-2.12-1.06 1.06L10.59 17zM19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m0 16H5V8h14z', - Help: 'M11 18h2v-2h-2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8m0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4', - Live: 'M21 6h-7.59l3.29-3.29L16 2l-4 4-4-4-.71.71L10.59 6H3c-1.1 0-2 .89-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.11-.9-2-2-2m0 14H3V8h18zM9 10v8l7-4z', - VOD: 'm 19.815223,2.0000416 a 0.58850091,0.49877638 0 0 0 -0.152559,0.017775 L 2.4317328,6.0126382 A 0.58850091,0.49877638 0 0 0 2.0210941,6.6257352 L 2.9224652,9.4191448 A 1.173617,0.82462271 0 0 0 2.09356,10.207336 1.173617,0.82462271 0 0 0 2.9599692,11.002528 V 22 h 1.176613 V 21.97845 H 20.824021 V 22 h 1.175977 V 10.712682 A 0.58850091,0.49877638 0 0 0 21.411374,10.212723 H 4.4410652 a 1.173617,0.82462271 0 0 0 0,-0.0054 1.173617,0.82462271 0 0 0 -0.069287,-0.2785336 l 16.0358828,-3.7173633 0.002,0.00646 1.134023,-0.2629113 -1.157542,-3.5869872 A 0.58850091,0.49877638 0 0 0 19.815315,2.0000062 Z M 19.407126,3.1109398 20.112078,5.2966424 4.0158064,9.0280154 3.3108555,6.8423128 Z m -2.292202,0.8544558 a 0.88775572,0.75240594 0 0 0 -0.19642,0.025866 0.88775572,0.75240594 0 0 0 -0.622314,0.9244907 0.88775572,0.75240594 0 0 0 1.090798,0.5268975 0.88775572,0.75240594 0 0 0 0.621679,-0.9239538 0.88775572,0.75240594 0 0 0 -0.893743,-0.5532946 z m -3.63663,0.8426032 A 0.88775572,0.75240594 0 0 0 13.281873,4.834396 0.88775572,0.75240594 0 0 0 12.660194,5.7588867 0.88775572,0.75240594 0 0 0 13.750358,6.2857842 0.88775572,0.75240594 0 0 0 14.372672,5.3618304 0.88775572,0.75240594 0 0 0 13.478294,4.8079988 Z M 9.7698324,5.6678394 a 0.88775572,0.75240594 0 0 0 -0.19642,0.02586 0.88775572,0.75240594 0 0 0 -0.621679,0.924494 A 0.88775572,0.75240594 0 0 0 10.041896,7.1450878 0.88775572,0.75240594 0 0 0 10.664211,6.2211341 0.88775572,0.75240594 0 0 0 9.7698324,5.6678394 Z M 5.8490599,6.5767097 A 0.88775572,0.75240594 0 0 0 5.6526401,6.6031068 0.88775572,0.75240594 0 0 0 5.0303252,7.5275985 0.88775572,0.75240594 0 0 0 6.1211239,8.054495 0.88775572,0.75240594 0 0 0 6.7434384,7.1305423 0.88775572,0.75240594 0 0 0 5.8490599,6.5767097 Z M 4.1365822,11.210483 H 20.824021 V 13.47646 H 4.1365822 Z m 2.3335209,0.292541 a 0.88775572,0.75240594 0 0 0 -0.8880218,0.752092 0.88775572,0.75240594 0 0 0 0.8880218,0.752633 0.88775572,0.75240594 0 0 0 0.888022,-0.752633 0.88775572,0.75240594 0 0 0 -0.888022,-0.752092 z m 4.0650679,0 a 0.88775572,0.75240594 0 0 0 -0.887386,0.752092 0.88775572,0.75240594 0 0 0 0.887386,0.752633 0.88775572,0.75240594 0 0 0 0.888022,-0.752633 0.88775572,0.75240594 0 0 0 -0.888022,-0.752092 z m 3.844493,0 a 0.88775572,0.75240594 0 0 0 -0.887387,0.752092 0.88775572,0.75240594 0 0 0 0.887387,0.752633 0.88775572,0.75240594 0 0 0 0.888022,-0.752633 0.88775572,0.75240594 0 0 0 -0.888022,-0.752092 z m 3.770121,0 a 0.88775572,0.75240594 0 0 0 -0.888022,0.752092 0.88775572,0.75240594 0 0 0 0.888022,0.752633 0.88775572,0.75240594 0 0 0 0.887386,-0.752633 0.88775572,0.75240594 0 0 0 -0.887386,-0.752092 z M 4.1365822,14.424658 H 20.824021 V 21.03025 H 4.1365822 Z m 5.7317781,0.759096 v 0.700912 4.107958 l 5.2219757,-2.554745 z m 1.0138837,1.402902 2.083069,0.898093 -2.083069,1.018233 z', - Series: 'M 5.6142482,2 A 0.52258163,0.5109162 0 0 0 5.0926858,2.5115754 V 6.8905936 H 2.521564 A 0.52258163,0.5109162 0 0 0 2.0000014,7.4027207 V 21.488425 A 0.52258163,0.5109162 0 0 0 2.521564,22 h 15.863059 a 0.52258163,0.5109162 0 0 0 0.522692,-0.511575 v -4.37957 h 2.569993 A 0.52258163,0.5109162 0 0 0 22,16.597831 V 2.5115754 A 0.52258163,0.5109162 0 0 0 21.477308,2 Z M 6.137504,3.0214951 H 20.955746 V 5.3426232 H 6.137504 Z M 8.2096417,3.3211558 A 0.78831624,0.7707189 0 0 0 7.4210893,4.0921057 0.78831624,0.7707189 0 0 0 8.2096417,4.8625039 0.78831624,0.7707189 0 0 0 8.9981944,4.0921057 0.78831624,0.7707189 0 0 0 8.2096417,3.3211558 Z m 3.6097313,0 a 0.78831624,0.7707189 0 0 0 -0.787989,0.7709499 0.78831624,0.7707189 0 0 0 0.787989,0.7703982 0.78831624,0.7707189 0 0 0 0.788552,-0.7703982 0.78831624,0.7707189 0 0 0 -0.788552,-0.7709499 z m 3.413862,0 a 0.78831624,0.7707189 0 0 0 -0.787988,0.7709499 0.78831624,0.7707189 0 0 0 0.787988,0.7703982 0.78831624,0.7707189 0 0 0 0.788553,-0.7703982 0.78831624,0.7707189 0 0 0 -0.788553,-0.7709499 z m 3.347821,0 A 0.78831624,0.7707189 0 0 0 17.792504,4.0921057 0.78831624,0.7707189 0 0 0 18.581056,4.8625039 0.78831624,0.7707189 0 0 0 19.369044,4.0921057 0.78831624,0.7707189 0 0 0 18.581056,3.3211558 Z M 6.137504,6.3138988 H 20.955746 V 13.080269 H 18.907315 V 7.4027207 A 0.52258163,0.5109162 0 0 0 18.384623,6.8905936 H 6.137504 Z M 3.0448198,7.9126405 H 17.863061 V 10.233768 H 3.0448198 Z M 5.1169577,8.211749 A 0.78831624,0.7707189 0 0 0 4.3284051,8.9826991 0.78831624,0.7707189 0 0 0 5.1169577,9.7536488 0.78831624,0.7707189 0 0 0 5.9055102,8.9826991 0.78831624,0.7707189 0 0 0 5.1169577,8.211749 Z m 3.6097308,0 A 0.78831624,0.7707189 0 0 0 7.9387002,8.9826991 0.78831624,0.7707189 0 0 0 8.7266885,9.7536488 0.78831624,0.7707189 0 0 0 9.5152413,8.9826991 0.78831624,0.7707189 0 0 0 8.7266885,8.211749 Z m 3.4138625,0 A 0.78831624,0.7707189 0 0 0 11.352563,8.9826991 0.78831624,0.7707189 0 0 0 12.140551,9.7536488 0.78831624,0.7707189 0 0 0 12.929104,8.9826991 0.78831624,0.7707189 0 0 0 12.140551,8.211749 Z m 3.34782,0 A 0.78831624,0.7707189 0 0 0 14.69982,8.9826991 0.78831624,0.7707189 0 0 0 15.488371,9.7536488 0.78831624,0.7707189 0 0 0 16.276361,8.9826991 0.78831624,0.7707189 0 0 0 15.488371,8.211749 Z M 3.0448198,11.205044 H 17.863061 v 6.765818 H 3.0448198 Z m 5.0897487,0.77702 v 0.718524 4.207942 l 4.6370505,-2.616926 z m 0.9003159,1.4376 1.8497406,0.919952 -1.8497406,1.043017 z m 9.8724306,0.632432 h 2.048431 v 2.035816 h -2.048431 v -0.327806 a 0.78831624,0.7707189 0 0 0 0.461729,-0.701967 0.78831624,0.7707189 0 0 0 -0.461729,-0.701416 z M 3.0448198,18.94269 H 17.863061 v 2.035815 H 3.0448198 Z m 2.0721379,0.235644 a 0.78831624,0.7707189 0 0 0 -0.7885526,0.770398 0.78831624,0.7707189 0 0 0 0.7885526,0.77095 0.78831624,0.7707189 0 0 0 0.7885525,-0.77095 0.78831624,0.7707189 0 0 0 -0.7885525,-0.770398 z m 3.6097308,0 a 0.78831624,0.7707189 0 0 0 -0.7879883,0.770398 0.78831624,0.7707189 0 0 0 0.7879883,0.77095 0.78831624,0.7707189 0 0 0 0.7885528,-0.77095 0.78831624,0.7707189 0 0 0 -0.7885528,-0.770398 z m 3.4138625,0 a 0.78831624,0.7707189 0 0 0 -0.787988,0.770398 0.78831624,0.7707189 0 0 0 0.787988,0.77095 0.78831624,0.7707189 0 0 0 0.788553,-0.77095 0.78831624,0.7707189 0 0 0 -0.788553,-0.770398 z m 3.34782,0 a 0.78831624,0.7707189 0 0 0 -0.788551,0.770398 0.78831624,0.7707189 0 0 0 0.788551,0.77095 0.78831624,0.7707189 0 0 0 0.78799,-0.77095 0.78831624,0.7707189 0 0 0 -0.78799,-0.770398 z', - Back: 'M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11', - TreeView: 'M17 12h2L12 2 5.05 12H7l-3.9 6h6.92v4h3.96v-4H21z', - Status: 'm 4,3.627 c -1.1,0 -2,0.8999999 -2,2 V 8.6269999 H 4 V 5.627 h 16 v 2.9999999 h 2 V 5.627 c 0,-1.1000001 -0.9,-2 -2,-2 z m 9.994135,3.041016 c -0.362495,0 -0.724532,0.1697839 -0.894533,0.5097642 L 10,13.386764 8.8906064,11.177778 A 0.98799998,0.98799998 0 0 0 8,10.627 H 2 v 2 h 5.3789124 l 1.7206895,3.451167 C 9.2796039,16.418167 9.6199993,16.627 10,16.627 c 0.380001,0 0.720606,-0.208833 0.890606,-0.548833 L 14,9.8672329 15.109393,12.078171 C 15.279391,12.418171 15.619999,12.627 16,12.627 h 6 v -2 H 16.619142 L 14.890606,7.1777802 C 14.720608,6.8377802 14.356611,6.668016 13.994135,6.668016 Z M 2,14.627 v 3 c 0,1.1 0.9,2 2,2 h 16 c 1.1,0 2,-0.9 2,-2 v -3 h -2 v 3 H 4 v -3 z' -} - - -Object.entries(DEFAULT_ICONS).forEach(icon_def => { - ICON[icon_def[0]] = createIcon(icon_def[0], icon_def[1]); -}); - - -export default ICON; diff --git a/frontend/src/index.scss b/frontend/src/index.scss deleted file mode 100755 index 5a54153c9..000000000 --- a/frontend/src/index.scss +++ /dev/null @@ -1,52 +0,0 @@ -@use 'scss/theme'; -@use 'scss/default'; - -html { - height: 100%; -} -body { - margin: 0; - padding: 0; - font-family: "Rubik", sans-serif; - background-color: var(--background-color); - height: 100%; - max-width: 100vw; - overflow: hidden; - box-sizing: border-box; - display: flex; - align-content: stretch; - align-items: stretch; - - input, textarea, select, button { - font-family: inherit; - } - - input { - height: 2rem; - border-radius: 6px; - } - - select { - border-color: var(--border-color); - } -} - -#root { - display: flex; - flex-flow: column; - flex: 1 1 auto; - box-sizing: border-box; - overflow: hidden; - max-width: 100%; - background-color: inherit; -} - -dialog { - border: none; - border-radius: var(--border-radius); - background-color: var(--card-background-color); - filter: drop-shadow(6px 6px 6px var(--popup-shadow-color)); - &::backdrop { - background: rgba(0, 0, 0, 0.3); /* Schwarzer, halbtransparenter Hintergrund */ - } -} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx deleted file mode 100755 index d3d9390e0..000000000 --- a/frontend/src/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import {createRoot} from 'react-dom/client'; -import './index.scss'; -import {SnackbarProvider} from 'notistack'; -import {ServiceProvider} from "./provider/service-provider"; -import Authentication from "./component/authentication/authentication"; -import Fetcher from "./utils/fetcher"; -import ServiceContext from "./service/service-context"; -import {UiConfig} from './model/ui-config'; -import i18n_init from "./utils/i18n"; - -import {catchError, map, switchMap} from 'rxjs/operators'; -import {EMPTY} from 'rxjs'; -import Tooltip from "./component/tooltip/tooltip"; - -const initUI = () => { - const container = document.getElementById('root'); - const root = createRoot(container); - root.render( - - - - - - - ); -} - -Fetcher.fetchJson("config.json").pipe( - switchMap((config: UiConfig) => { - ServiceContext.config().setUiConfig(config); - const document_title = config.tab_title || config.app_title; - if (document_title) { - document.title = document_title; - } - return i18n_init(ServiceContext.config().getUiConfig().languages).pipe( - map(() => { - initUI(); - }) - ) - } - ), - catchError((error: Error) => { - initUI(); - return EMPTY; - }) -).subscribe(); - -// Fetcher.fetchJson("config.json").subscribe({ -// next: (config: UiConfig) => { -// i18n_init(config.languages).subscribe( -// ServiceContext.config().setUiConfig(config); -// initUI(); -// }, -// error: (error: Error) => {initUI()} -// }); - diff --git a/webui/src/lib.rs b/frontend/src/lib.rs similarity index 100% rename from webui/src/lib.rs rename to frontend/src/lib.rs diff --git a/webui/src/main.rs b/frontend/src/main.rs similarity index 100% rename from webui/src/main.rs rename to frontend/src/main.rs diff --git a/webui/src/model/busy_status.rs b/frontend/src/model/busy_status.rs similarity index 100% rename from webui/src/model/busy_status.rs rename to frontend/src/model/busy_status.rs diff --git a/webui/src/model/dialog.rs b/frontend/src/model/dialog.rs similarity index 100% rename from webui/src/model/dialog.rs rename to frontend/src/model/dialog.rs diff --git a/webui/src/model/event_message.rs b/frontend/src/model/event_message.rs similarity index 100% rename from webui/src/model/event_message.rs rename to frontend/src/model/event_message.rs diff --git a/webui/src/model/explorer_source_type.rs b/frontend/src/model/explorer_source_type.rs similarity index 100% rename from webui/src/model/explorer_source_type.rs rename to frontend/src/model/explorer_source_type.rs diff --git a/frontend/src/model/file-download.ts b/frontend/src/model/file-download.ts deleted file mode 100644 index d148e5121..000000000 --- a/frontend/src/model/file-download.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface FileDownloadRequest { - url: string; - filename: string; -} - -export interface FileDownloadInfo { - uuid: string; - ts_created?: number; - ts_modified?: number; - filename?: string; - finished?: boolean; - filesize?: number; - error?: string; -} - -export interface DownloadInfo { - completed: boolean; - active?: FileDownloadInfo; - downloads?: FileDownloadInfo[]; -} \ No newline at end of file diff --git a/frontend/src/model/message-kind.ts b/frontend/src/model/message-kind.ts deleted file mode 100644 index 2a0509b56..000000000 --- a/frontend/src/model/message-kind.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum MessageKind { - Info = "info", - Stats = "stats", - Error = "error", - Watch = "watch", -} \ No newline at end of file diff --git a/webui/src/model/mod.rs b/frontend/src/model/mod.rs similarity index 100% rename from webui/src/model/mod.rs rename to frontend/src/model/mod.rs diff --git a/frontend/src/model/playlist-request.ts b/frontend/src/model/playlist-request.ts deleted file mode 100644 index ace54e614..000000000 --- a/frontend/src/model/playlist-request.ts +++ /dev/null @@ -1,15 +0,0 @@ -export enum PlaylistRequestType { - INPUT = 1, - TARGET = 2, - XTREAM= 3, - M3U= 4 -} - -export interface PlaylistRequest { - rtype: PlaylistRequestType; - sourceName?: string; - username?: string; - password?: string; - url?: string; - sourceId?: number, -} \ No newline at end of file diff --git a/frontend/src/model/playlist.ts b/frontend/src/model/playlist.ts deleted file mode 100755 index ce30fd708..000000000 --- a/frontend/src/model/playlist.ts +++ /dev/null @@ -1,81 +0,0 @@ -export interface PlaylistItemHeader { - id: number; - name: string; - logo: string; - logo_small: string; - group: string; - title: string; - parent_code: string; - audio_track: string; - time_shift: string; - rec: string; - source: string; - url: string; -} - -export enum XtreamCluster { - Live = 'Live', - Video = 'Video', - Series = 'Series', -} - -export enum PlaylistItemType { - Live = 'Live', - Video = 'Video', - Series = 'Series', // xtream series description - SeriesInfo = 'SeriesInfo', // xtream series info fetched for series description - Catchup = 'Catchup', - LiveUnknown = 'LiveUnknown', // No Provider id - LiveHls = 'LiveHls', // m3u8 entry -} - -export interface PlaylistItem { - id: number; - category_id: number, - provider_id: number, - virtual_id: number, - url: string, - name: string, - title: string, - channel_no: number, - epg_channel_id: string, - group: string, - input_name: string, - item_type: PlaylistItemType, - logo: string, - logo_small: string, - xtream_cluster: XtreamCluster, - additional_properties: string, -} - -export interface PlaylistGroup { - id: number, - name: string, - channels?: PlaylistItem[]; -} - -export interface PlaylistCategories { - live: PlaylistGroup[], - vod: PlaylistGroup[], - series: PlaylistGroup[], -} - -export enum PlaylistCategory { - LIVE = 'live', - VOD = 'vod', - SERIES = 'series' -} - -export const EmptyPlaylistCategories: PlaylistCategories = {live: [], vod: [], series: []} - - -export interface UserPlaylistTargetCategories { - live: string[], - vod: string[], - series: string[], -} - -export interface UserPlaylistCategories { - xtream: UserPlaylistTargetCategories, - m3u: UserPlaylistTargetCategories, -} \ No newline at end of file diff --git a/frontend/src/model/server-config.ts b/frontend/src/model/server-config.ts deleted file mode 100644 index 02a618bf9..000000000 --- a/frontend/src/model/server-config.ts +++ /dev/null @@ -1,285 +0,0 @@ -export enum InputType { - m3u = "m3u", - xtream = "xtream" -} - -export enum SortOrder { - asc = "asc", - desc = "desc" -} - -export enum ExportStyle { - kodi = "kodi", - plex = "plex", - emby = "emby", - jellyfin = "jellyfin" -} - -export enum TargetType { - m3u = "m3u", - xtream = "xtream", - strm = "strm", - hdhomerun = "hdhomerun" -} - -export enum ProcessingOrder { - frm = "frm", - fmr = "fmr", - rfm = "rfm", - rmf = "rmf", - mfr = "mfr", - mrf = "mrf", -} - -export interface InputConfig { - id: number, - input_type: InputType, - url: string, - username: string, - password: string, - persist: string, - name: string, - enabled: boolean - options: { - xtream_skip_live: boolean, - xtream_skip_vod: boolean, - xtream_skip_series: boolean, - }, -} - -export interface TargetConfig { - enabled: boolean, - name: string, - options: { - ignore_logo: boolean, - share_live_streams: boolean, - remove_duplicates: boolean, - force_redirect?: string, - }, - sort: { - match_as_ascii: boolean, - groups: { - order: SortOrder - }, - channels: - { - field: string, - group_pattern: string, - order: SortOrder - }[] - }, - filter: string, - output: [ - { - type: TargetType, - // m3u - filename: string - include_type_in_url: boolean, - mask_redirect_url: boolean, - // xtream - skip_live_direct_source: boolean, - skip_video_direct_source: boolean, - skip_series_direct_source: boolean, - resolve_series: boolean, - resolve_series_delay: number, - resolve_vod: boolean, - resolve_vod_delay: number, - // strm - style: ExportStyle, - flat?: boolean, - cleanup: boolean, - directory: string, - username?: string, // hdhomerun & strm - underscore_whitespace: boolean, - strm_props?: string[], - /** @deprecated Use style="kodi" instead */ - kodi_style?: boolean, - // hdhomerun - device: String, - use_output?: TargetType, - } - ], - rename: [ - { - field: string, - pattern: string, - new_name: string - } - ], - mapping: string[], - processing_order: ProcessingOrder, - watch: string[] -} - -export interface SourceConfig { - inputs: InputConfig[]; - targets: TargetConfig[]; -} - -export interface VideoDownloadConfig { - headers: Record, - directory: string; - organize_into_directories: boolean; - episode_pattern: string; -} - -export interface VideoConfig { - extensions: string[]; - download?: VideoDownloadConfig, - web_search?: string; -} - -export interface TelegramConfig { - bot_token: string; - chat_ids: string[]; -} - -export interface RestConfig { - url: string; -} - -export interface PushoverConfig { - url?: string; - token: string; - user: string; -} - -export interface MessagingConfig { - notify_on: string[]; - telegram?: TelegramConfig; - rest?: RestConfig; - pushover?: PushoverConfig; -} - -export interface LogConfig { - sanitize_sensitive_info: boolean, - log_active_user: boolean, - log_level?: string, -} - -export interface Credentials { - username: string; - password: string; - token: string; - server: string; - proxy: 'redirect' | 'reverse'; - created_at: number, - exp_date: number, - max_connections: number, - status: string, - ui_enabled: boolean, - comment: string, -} - -export interface TargetUser { - target: string; - credentials: Credentials[]; -} - -export interface ApiProxyServerInfo { - name: string; - protocol: string; - host: string; - port: string; - timezone: string; - message: string; - path: string; -} - -export interface ApiProxyConfig { - server: ApiProxyServerInfo[]; - user: TargetUser[]; -} - -export interface ServerApiConfig { - host: string; - port: number; - web_root: string -} - -export interface Schedule { - schedule: string; - targets: string[]; -} - -export interface WebAuthConfig { - enabled: boolean, - issuer: string, - secret: string, - userfile?: string, -} - -export interface StreamBufferConfig { - enabled: boolean, - size: number, -} - -export interface StreamConfig { - retry: boolean, - buffer?: StreamBufferConfig, - throttle?: string, - grace_period_millis?: number, - grace_period_timeout_secs?: number, -} - -export interface CacheConfig { - enabled: boolean, - size?: string, - dir?: string, -} - -export interface RateLimitConfig { - enabled: boolean, - period_millis: number, - burst_size: number, -} -export interface ReverseProxyConfig { - stream?: StreamConfig, - cache?: CacheConfig, - rate_limit?: RateLimitConfig, - resource_rewrite_disabled: boolean, - disable_referer_header: boolean, -} - -export interface WebUiConfig { - enabled: boolean, - user_ui_enabled: boolean, - path?: string, - auth?: WebAuthConfig, - player_server?: string, -} - -export interface ProxyConfig { - url: string, - username?: string, - password?: string, -} - -export interface IpCheckConfig { - url?: string, - url_ipv4?: string, - url_ipv6?: string, - pattern_ipv4?: string, - pattern_ipv6?: string, -} - -export interface ServerMainConfig { - threads: number; - api: ServerApiConfig; - working_dir: string; - backup_dir: string; - schedules?: Schedule[]; - messaging?: MessagingConfig; - video?: VideoConfig; - log?: LogConfig, - update_on_boot: boolean, - web_ui?: WebUiConfig, - reverse_proxy?: ReverseProxyConfig, - proxy?: ProxyConfig, - ipcheck?: IpCheckConfig, -} - -export default interface ServerConfig extends ServerMainConfig { - sources: SourceConfig[]; - api_proxy?: ApiProxyConfig; -} \ No newline at end of file diff --git a/frontend/src/model/server-status.tsx b/frontend/src/model/server-status.tsx deleted file mode 100644 index 664ae1e3a..000000000 --- a/frontend/src/model/server-status.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export interface ServerStatus { - status: string, - version: string, - build_time: string, - server_time: string, - memory: string, - cache: string, - active_user: number, - active_user_connections: number, - active_provider_connections: Record, -} - -export interface ServerIpCheck { - ipv4?: string, - ipv6?: string, -} \ No newline at end of file diff --git a/frontend/src/model/tree-selection.ts b/frontend/src/model/tree-selection.ts deleted file mode 100755 index 6c1c2bfd4..000000000 --- a/frontend/src/model/tree-selection.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default interface TreeSelection { - id?: number; - text: string; - type: 'group' | 'entry'; -} \ No newline at end of file diff --git a/frontend/src/model/ui-config.ts b/frontend/src/model/ui-config.ts deleted file mode 100644 index aa5439234..000000000 --- a/frontend/src/model/ui-config.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface UiConfig { - languages?: string[], - tab_title?: string, - app_title?: string, - app_logo?: string, - api?: { - apiUrl?: string - authUrl?: string - } -} - -export const DefaultUiConfig: UiConfig = { - languages: ["en", "de", "fr", "tr", "es", "it", "ru"], - app_title: "tuliprox", - api: { - apiUrl: "/api/v1/", - authUrl: "/auth/" - } -} \ No newline at end of file diff --git a/webui/src/model/view_type.rs b/frontend/src/model/view_type.rs similarity index 100% rename from webui/src/model/view_type.rs rename to frontend/src/model/view_type.rs diff --git a/webui/src/model/web_config.rs b/frontend/src/model/web_config.rs similarity index 100% rename from webui/src/model/web_config.rs rename to frontend/src/model/web_config.rs diff --git a/webui/src/provider/dialog_provider.rs b/frontend/src/provider/dialog_provider.rs similarity index 100% rename from webui/src/provider/dialog_provider.rs rename to frontend/src/provider/dialog_provider.rs diff --git a/webui/src/provider/icon_context_provider.rs b/frontend/src/provider/icon_context_provider.rs similarity index 100% rename from webui/src/provider/icon_context_provider.rs rename to frontend/src/provider/icon_context_provider.rs diff --git a/webui/src/provider/mod.rs b/frontend/src/provider/mod.rs similarity index 100% rename from webui/src/provider/mod.rs rename to frontend/src/provider/mod.rs diff --git a/frontend/src/provider/service-provider.tsx b/frontend/src/provider/service-provider.tsx deleted file mode 100644 index 3b5e5fea4..000000000 --- a/frontend/src/provider/service-provider.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, {useContext} from "react"; -import ServiceContext, {Services} from "../service/service-context"; - -const ServiceCtx = React.createContext(ServiceContext); -const ServiceCtxProvider = ServiceCtx.Provider; - -export function ServiceProvider(props: any) { - return {props.children} -} - -export function useServices(): Services { - return useContext(ServiceCtx); -} \ No newline at end of file diff --git a/webui/src/provider/service_context_provider.rs b/frontend/src/provider/service_context_provider.rs similarity index 100% rename from webui/src/provider/service_context_provider.rs rename to frontend/src/provider/service_context_provider.rs diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts deleted file mode 100644 index 6431bc5fc..000000000 --- a/frontend/src/react-app-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/frontend/src/scss/_common.scss b/frontend/src/scss/_common.scss deleted file mode 100644 index d44830f99..000000000 --- a/frontend/src/scss/_common.scss +++ /dev/null @@ -1,134 +0,0 @@ -$media-size-mobile: 600px; - -.hidden { - display: none; -} - -@mixin container_common($justify-content: flex-start, $padding: 12px) { - display: flex; - justify-content: $justify-content; - gap: 8px; - box-sizing: border-box; - @media (max-width: $media-size-mobile) { - padding: $padding; - } - @media (min-width: $media-size-mobile) { - padding: 2px; - } -} - -@mixin container_vert() { - flex-flow: column; - @include container_common(); -} - -@mixin container_horz() { - flex-flow: row; - @include container_common(); -} - -@mixin container_horz_nowrap($justify-content: flex-start, $padding: 12px) { - flex-flow: row nowrap; - @media (max-width: $media-size-mobile) { - flex-flow: column; - } - @include container_common($justify-content, $padding); -} - -@mixin container_horz_no_padding() { - flex-flow: row; - @include container_common($padding: 0); -} - -@mixin container_vert_no_padding($justify-content: flex-start) { - flex-flow: column; - @include container_common($justify-content, $padding: 0); -} - -@mixin activeOptionButton { - box-sizing: border-box !important; - background-color: var(--option-active-background-color) !important; - color: var(--option-active-color) !important; - font-weight: bold; - svg { - color: var(--option-active-color) !important; - } -} - -@mixin iconButton { - display: flex; - justify-content: center; - align-items: center; - color: var(--icon-button-color); - background-color: var(--icon-button-background-color); - border: none; - border-radius: var(--icon-button-border-radius); - aspect-ratio: 1 / 1; - cursor: pointer; - box-sizing: border-box; - max-height: 100%; - width: min-content; - - svg { - color: var(--text-color); - } - &:hover { - svg { - color: var(--text-button-hover-color); - } - } - - &:focus, - &:hover { - color: var(--icon-button-hover-color); - background-color: var(--icon-button-hover-background-color); - outline: none; - } -} - -@mixin textButton { - border: 1px solid var(--text-button-border-color); - font-size: 1.2rem; - border-radius: var(--text-button-border-radius); - padding: 8px 16px; - background-color: var(--text-button-background-color); - color: var(--text-button-color); - &:focus, - &:hover { - background-color: var(--text-button-hover-background-color); - color: var(--text-button-hover-color); - border: 1px solid transparent; - outline: none; - } -} - -@mixin list { - ul { - list-style-type: none; /* Remove bullets */ - padding: 0; /* Remove padding */ - margin: 0; /* Remove margins */ - li { - padding: 8px; - border-radius: var(--border-radius); - } - } -} - -@mixin list-hover { - ul { - list-style-type: none; /* Remove bullets */ - padding: 0; /* Remove padding */ - margin: 0; /* Remove margins */ - li { - padding: 8px; - border-radius: var(--border-radius); - user-select: none; - cursor: pointer; - - &:hover { - color: var(--popup-hover-color); - background-color: var(--popup-hover-background-color); - } - } - } -} \ No newline at end of file diff --git a/frontend/src/scss/_default.scss b/frontend/src/scss/_default.scss deleted file mode 100644 index 74fe1d26e..000000000 --- a/frontend/src/scss/_default.scss +++ /dev/null @@ -1,123 +0,0 @@ -@use './common'; - -::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - border-radius: 10px; - background-color: var(--scrollbar-track-color); -} - -::-webkit-scrollbar { - width: 12px; - background-color: transparent; -} - -::-webkit-scrollbar-thumb { - border-radius: 10px; - //-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); - background-color: var(--scrollbar-handle-color); -} - - -button { - cursor: pointer; - //&:active { - // transform: scale(0.90); /* Scaling button to 0.98 to its original size */ - //} -} - -.tp__hidden { - display: none !important; -} - -.card { - display: flex; - flex-flow: column; - flex: 0 1 auto; - align-content: flex-start; - overflow: hidden; - padding: 20px; - border: 1px solid var(--card-border-color); - background-color: var(--card-background-color); - color: var(--card-color); - filter: drop-shadow(3px 3px 4px var(--popup-shadow-color)); - min-width: 200px; - - border-radius:var(--border-radius); - - @media (min-width: common.$media-size-mobile) { - padding: 2px; - } -} - -.vert-group { - display: flex; - flex-flow: column; - gap: 20px; - overflow: hidden; - padding: 0 4px 4px 0; -} - -select, -input { - color: var(--text-color); - &:focus { - background-color: var(--background-color) !important; - } -} - -select { - padding: 2px 4px; -} - -.tool-button { - display: flex; - align-items: center; - justify-content: center; - height: 1.1rem; - width: 1.1rem; - border-radius: var(--border-radius); - background-color: var(--tool-button-background-color); - color: var(--tool-button-color); - cursor: pointer; - - svg { - width: 1rem; - height: 1rem; - } - - &:hover { - color: var(--tool-button-hover-color); - background-color: var(--tool-button-hover-background-color); - } - - &:active { - transform: scale(0.9); - } -} - -:not(input[type="text"]) { - caret-color: transparent; -} -input[type="text"] { - caret-color: auto; -} - -select { - height: 2rem; - border-radius: var(--border-radius); - font-size: 1.2rem; - padding: 0 8px; - cursor: pointer; - &:hover { - color: var(--text-button-hover-color); - background-color: var(--text-button-hover-background-color); - } -} - -option { - cursor: pointer; -} - -dialog { - padding: 0 !important; -} \ No newline at end of file diff --git a/frontend/src/scss/_panel.scss b/frontend/src/scss/_panel.scss deleted file mode 100644 index 1c2408b4b..000000000 --- a/frontend/src/scss/_panel.scss +++ /dev/null @@ -1,6 +0,0 @@ -@use "./common"; - -.panel { - @include common.container_vert_no_padding(); - overflow: hidden; -} diff --git a/frontend/src/scss/_preferences.scss b/frontend/src/scss/_preferences.scss deleted file mode 100644 index 15b5b8d2f..000000000 --- a/frontend/src/scss/_preferences.scss +++ /dev/null @@ -1,53 +0,0 @@ -@use './common'; - -@mixin prefsRoot { - @include common.container_vert_no_padding(flex-start); - align-content: flex-start; - overflow: hidden; -} - -@mixin prefsToolbar() { - &__toolbar { - display: flex; - flex-flow: row wrap; - flex: 0 0 auto; - justify-content: flex-end; - align-items: center; - gap: 12px; - background-color: var(--toolbar-bg-color); - padding: 8px 16px; - - label { - flex: 1 1 auto; - font-size: 2rem; - font-weight: bold; - color: var(--title-label-color); - } - - button { - @include common.textButton(); - } - } -} - -@mixin prefsForm() { - &__content { - display: flex; - flex-flow: column; - - &-help { - display: flex; - flex-flow: column; - gap:12px; - border-top: 1px solid var(--border-light-color); - padding-top: 12px; - color: var(--label-color); - - pre { - margin: 0; - } - } - - } - -} \ No newline at end of file diff --git a/frontend/src/scss/_theme.scss b/frontend/src/scss/_theme.scss deleted file mode 100644 index 9089edeb6..000000000 --- a/frontend/src/scss/_theme.scss +++ /dev/null @@ -1,110 +0,0 @@ -:root { - --media-size-mobile: 720px; - --text-color: #ffffff; - --hint-color: #888888; - --label-color: #73d6fe; - --background-color: #303946; //#121212; - --app-header-color: #73d6fe; //#6495b1; - --sidebar-toggle-color: #ffffff; - --sidebar-toggle-hover-color: #47495d; - --playlist-color: #aeb0b2; - --border-color: #47495d; - --border-light-color: #68686c; - --hover-border-color: #aaaab0; - --spinner-color: #ffffff66; - --spinner-highlight-color: #5599f0; - --tree-count-color: #7b7b7e; - --tree-hover-background-color: #004db4; - --tree-hover-color: white; - --tree-expander-hover-color: white; - --tool-button-background-color: #434343; - --tool-button-color: #fff; - --tool-button-hover-color: #ffffff; - --tool-button-hover-background-color: #0c8a13; - --default-button-background-color: #124a9b; - - --toolbar-background-color: #282A36; - --toolbar-button-hover-color: #ffffff; - --toolbar-button-hover-background-color: #004db4; - - //--popup-background-color: #282A36; - --popup-background-color: #353535; - --popup-shadow-color: #06070a; - --popup-border-color: #646464; - --popup-color: #fff; - --popup-hover-background-color: #5599f0; - --popup-hover-color: #fff; - - --attention-color: red; - --warn-color: orange; - --ok-color: #21e821; - - --card-background-color: #202733; //#282828; - --card-background-color-darker: #1c2026; - --card-color: #ffffff; - --card-border-color: #47495d; - - --title-label-color: #6495b1; - - --border-radius: 8px; - - --icon-button-border-radius: 4px; - --icon-button-background-color: var(--default-button-background-color); - --icon-button-color: #ffffff; - --icon-button-hover-background-color: #0d6efd; - --icon-button-hover-color: #ffffff; - --icon-button-hover-border-color: #ced9fb; - - --menu-button-color:#ffffff; - --menu-button-background-color: #20273344; - - --menu-button-hover-color: #ffffff; - --menu-button-hover-background-color: #202733; - - --text-button-border-radius: 4px; - --text-button-border-color: transparent; - --text-button-background-color: var(--default-button-background-color); - --text-button-color: #ffffff; - --text-button-hover-background-color: #0d6efd; - --text-button-hover-color: #ffffff; - - --scrollbar-track-color: #838383; - --scrollbar-handle-color: #505050; - - --tabset-background-color: #44475a47; // #282A36; - --tabset-border-color: #535353; - --tab-background-color: #44475A; - --tab-color: #fff; - --tab-hover-background-color: #6272A4; - --tab-hover-color: #50FA7B; - --tab-avtive-indicator-color: #50FA7B; - --tab-active-background-color: #54547a; - --tab-active-color: #50FA7B; - - --table-border-color: #44475a; - - --checkbox-background-color: #44475A; - --checkbox-hover-background-color: #006fee; - --checkbox-checked-background-color: #006fee; - --checkbox-border-radius: 4px; - - --option-active-background-color: #50FA7B; - --option-active-color: #000; - - --shocase-card-background: #4b4b4b; - - --breadcrumbs-background-color: #202733; - --breadcrumb-background-color: #4e545d; - --breadcrumb-color: #c4c4c4; - --breadcrumb-active-background-color: #4ad96e; - --breadcrumb-active-color: #000; - - --menu-icon-color-update: #A3D8FF; - --menu-icon-color-user: #0ABAB5; - --menu-icon-color-api_server: #FF9D23; - --menu-icon-color-main_config: #FF2DF1; - --menu-icon-color-status : #F9E400; - - --toolbar-bg-color: #161b25; - -} diff --git a/frontend/src/service/auth-service.ts b/frontend/src/service/auth-service.ts deleted file mode 100644 index be9872a5f..000000000 --- a/frontend/src/service/auth-service.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - catchError, - concatWith, EMPTY, - interval, - map, - Observable, - of, - ReplaySubject, - takeWhile, - tap, - throwError -} from "rxjs"; -import AuthApiService, {DefaultAuthApiService} from "../api/auth-api-service"; -import {first} from "rxjs/operators"; -import {jwtDecode} from "jwt-decode"; - -export enum UserRole { - NONE = 0, - ADMIN = 1, - USER= 2, -} - -const AUTH_TOKEN_KEY = "auth-token"; -const NO_AUTHORIZATION_TOKEN = "authorized"; - -const REFRESH_INTERVAL = 1000 * 60 * 15; // 15 mins - -export default class AuthService { - - private token: string; - private subject = new ReplaySubject(UserRole.NONE); - - constructor(private authApiService: AuthApiService = new DefaultAuthApiService()) { - this.token = localStorage.getItem(AUTH_TOKEN_KEY); - interval(REFRESH_INTERVAL).pipe( - takeWhile(() => this.token !== NO_AUTHORIZATION_TOKEN), - concatWith(EMPTY)).subscribe(() => this.refresh().pipe(first()).subscribe({ - next: (success: boolean) => { - if (success) { - this.subject.next(this.getRole()); - } else { - this.clearToken(); - } - }, - error: () => this.clearToken(), - })); - } - - private clearToken() { - localStorage.removeItem(AUTH_TOKEN_KEY); - this.subject.next(UserRole.NONE); - } - - private getRole(): UserRole { - if (this.token) { - try { - if (this.token === NO_AUTHORIZATION_TOKEN) { - return UserRole.ADMIN; - } - const claims: any = jwtDecode(this.token); - if (claims?.roles?.includes("ADMIN")) { - return UserRole.ADMIN; - } - if (claims?.roles?.includes("USER")) { - return UserRole.USER; - } - } catch(err) { - this.clearToken(); - } - } - return UserRole.NONE; - } - - authChannel(): Observable { - return this.subject; - } - - refresh(): Observable { - if (this.token) { - return this.authApiService.refresh().pipe(map(auth => { - this.token = auth.token; - localStorage.setItem(AUTH_TOKEN_KEY, auth.token); - // eslint-disable-next-line eqeqeq - return auth.token != undefined - }), tap(data => { - this.subject.next(this.getRole()); - }), catchError((error: any) => { - this.subject.next(UserRole.NONE); - return throwError(() => error) - })); - } - return of(false); - } - - authenticate(username: string, password: string): Observable { - return this.authApiService.authenticate(username, password).pipe(map(auth => { - this.token = auth.token; - localStorage.setItem(AUTH_TOKEN_KEY, auth.token); - // eslint-disable-next-line eqeqeq - return auth.token != undefined - }), tap(data => { - this.subject.next(this.getRole()); - }), catchError(() => { - this.clearToken(); - return of(false); - })); - } - - isAuthenticated(): boolean { - // eslint-disable-next-line eqeqeq - return this.token != undefined; - } - - getToken(): string { - return this.token; - } - - logout() { - this.token = null; - localStorage.removeItem(AUTH_TOKEN_KEY); - this.subject.next(UserRole.NONE); - } -} \ No newline at end of file diff --git a/frontend/src/service/config-service.ts b/frontend/src/service/config-service.ts deleted file mode 100644 index 77dca1212..000000000 --- a/frontend/src/service/config-service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import ConfigApiService, {DefaultConfigApiService} from "../api/config-api-service"; -import {Observable} from "rxjs"; -import ServerConfig, {ApiProxyServerInfo, ServerMainConfig, TargetUser} from "../model/server-config"; -import {DefaultUiConfig, UiConfig} from "../model/ui-config"; - -export default class ConfigService { - - private uiConfig: UiConfig = DefaultUiConfig; - - constructor(private configApiService: ConfigApiService = new DefaultConfigApiService()) { - } - - setUiConfig(uiConfig: UiConfig): void { - if (uiConfig) { - this.uiConfig = {...DefaultUiConfig, ...uiConfig}; - } - } - - getUiConfig(): UiConfig { - return this.uiConfig; - } - - getServerConfig(): Observable { - return this.configApiService.getServerConfig(); - } - - saveMainConfig(config: ServerMainConfig) { - return this.configApiService.saveMainConfig(config); - } - - saveTargetUser(targetUser: TargetUser[]) { - return this.configApiService.saveTargetUser(targetUser); - } - - saveApiProxyConfig(serverInfo: ApiProxyServerInfo[]) { - return this.configApiService.saveApiProxyConfig(serverInfo); - } -} diff --git a/frontend/src/service/file-service.ts b/frontend/src/service/file-service.ts deleted file mode 100755 index 2efc85cff..000000000 --- a/frontend/src/service/file-service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {Observable, Subject, Subscription, tap} from "rxjs"; -import {DownloadInfo, FileDownloadInfo, FileDownloadRequest} from "../model/file-download"; -import FileApiService, { DefaultFileApiService } from "../api/file-api-service"; - -export default class FileService { - - private downloadNotification = new Subject(); - constructor(private fileApiService: FileApiService = new DefaultFileApiService()) { - } - - subscribeDownloadNotification(observer: (value: T) => void): Subscription { - return this.downloadNotification.subscribe(observer as any); - } - - private notifyDownload(info: FileDownloadInfo) { - this.downloadNotification.next(info); - } - - download(req: FileDownloadRequest): Observable { - return this.fileApiService.download(req).pipe(tap((result) => this.notifyDownload(result) )); - } - - getDownloadInfo(): Observable { - return this.fileApiService.getDownloadInfo(); - } -} - diff --git a/frontend/src/service/playlist-service.ts b/frontend/src/service/playlist-service.ts deleted file mode 100644 index 8ed8014eb..000000000 --- a/frontend/src/service/playlist-service.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {Observable} from "rxjs"; -import PlaylistApiService, {DefaultPlaylistApiService} from "../api/playlist-api-service"; -import {first} from "rxjs/operators"; -import {PlaylistRequest} from "../model/playlist-request"; -import {PlaylistCategories, PlaylistGroup, PlaylistItem} from "../model/playlist"; - -// const mergeCategory = (groups: PlaylistGroup[], channels: PlaylistItem[]) => { -// if (groups?.length && channels?.length) { -// let unknown: PlaylistGroup = { -// id: 0, -// name: "Unknown", -// channels: [], -// }; -// const groupMap = groups.reduce((acc: any, group: PlaylistGroup) => { -// acc[group.name] = group; -// group.channels = group.channels ?? []; -// return acc; -// }, {}); -// channels.forEach(channel => { -// let group = groupMap[channel.group] ?? unknown; -// group.channels.push(channel); -// }); -// } -// } - -export default class PlaylistService { - - constructor(private playlistApiService: PlaylistApiService = new DefaultPlaylistApiService()) { - } - - getPlaylistCategories(req: PlaylistRequest): Observable { - return new Observable((obs) => - this.playlistApiService.getPlaylist(req).pipe(first()).subscribe({ - next: (pl: PlaylistCategories) => { - if (pl) { - let cnt = 0; - [pl?.live, pl?.vod, pl?.series].filter(Boolean).flat().forEach((g: PlaylistGroup) => { - g.id = ++cnt; - g.name = g.name ?? (g as any).title; - g?.channels?.forEach(c => { - c.id = ++cnt; - }); - }); - obs.next(pl); - } else { - obs.error("Could not download playlist"); - } - }, - error: (e) => obs.error(e), - complete: () => obs.complete(), - })); - } - - update(targets: string[]): Observable { - return this.playlistApiService.updateTargets(targets); - } - - getWebPlayerUrl(item: PlaylistItem, req: PlaylistRequest): Observable { - return this.playlistApiService.getWebPlayerUrl(item, req); - } - -} diff --git a/frontend/src/service/server-status-service.ts b/frontend/src/service/server-status-service.ts deleted file mode 100644 index cbbfd1e4f..000000000 --- a/frontend/src/service/server-status-service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {Observable} from "rxjs"; -import {ServerIpCheck, ServerStatus} from "../model/server-status"; -import ServerStatusApiService, {DefaultServerStatusApiService} from "../api/server-status-api-service"; - -export default class ServerStatusService { - - constructor(private serverStatusApiService: ServerStatusApiService = new DefaultServerStatusApiService()) { - } - - getServerStatus(): Observable { - return this.serverStatusApiService.getServerStatus(); - } - getIpCheck(): Observable { - return this.serverStatusApiService.getServerIpCheck(); - } -} diff --git a/frontend/src/service/service-context.ts b/frontend/src/service/service-context.ts deleted file mode 100644 index 35c35642c..000000000 --- a/frontend/src/service/service-context.ts +++ /dev/null @@ -1,58 +0,0 @@ -import ConfigService from "./config-service"; -import PlaylistService from "./playlist-service"; -import FileService from "./file-service"; -import AuthService from "./auth-service"; -import UserConfigService from "./user-config-service"; -import ServerStatusService from "./server-status-service"; - -export interface Services { - config(): ConfigService; - - playlist(): PlaylistService; - - file(): FileService; - - auth(): AuthService; - - userConfig(): UserConfigService; - - status(): ServerStatusService; -} - -class ServiceContextImpl implements Services { - - private readonly _configService: ConfigService = new ConfigService(); - private readonly _playlistService: PlaylistService = new PlaylistService(); - private readonly _fileService: FileService = new FileService(); - private readonly _authService: AuthService = new AuthService(); - private readonly _userConfigService: UserConfigService = new UserConfigService(); - private readonly _statusService: ServerStatusService = new ServerStatusService(); - - config() { - return this._configService; - } - - playlist() { - return this._playlistService; - } - - file() { - return this._fileService; - } - - auth() { - return this._authService; - } - - userConfig(): UserConfigService { - return this._userConfigService; - } - - status(): ServerStatusService { - return this._statusService; - } -} - -const ServiceContext = new ServiceContextImpl(); -export default ServiceContext; - diff --git a/frontend/src/service/user-config-service.ts b/frontend/src/service/user-config-service.ts deleted file mode 100644 index 2e2ddca94..000000000 --- a/frontend/src/service/user-config-service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Observable } from "rxjs"; -import UserConfigApiService, {DefaultUserConfigApiService} from "../api/user-config-api-service"; -import {UserPlaylistCategories} from "../model/playlist"; - -export default class UserConfigService { - constructor(private userConfigApiService: UserConfigApiService = new DefaultUserConfigApiService()) { - } - - getPlaylistBouquet(): Observable { - return this.userConfigApiService.getPlaylistBouquet(); - } - getPlaylistCategories(): Observable { - return this.userConfigApiService.getPlaylistCategories(); - } - - savePlaylistBouquet(bouquet: UserPlaylistCategories): Observable { - return this.userConfigApiService.savePlaylistBouquet(bouquet); - } -} diff --git a/webui/src/services/auth_service.rs b/frontend/src/services/auth_service.rs similarity index 100% rename from webui/src/services/auth_service.rs rename to frontend/src/services/auth_service.rs diff --git a/webui/src/services/config_service.rs b/frontend/src/services/config_service.rs similarity index 100% rename from webui/src/services/config_service.rs rename to frontend/src/services/config_service.rs diff --git a/webui/src/services/dialog_service.rs b/frontend/src/services/dialog_service.rs similarity index 100% rename from webui/src/services/dialog_service.rs rename to frontend/src/services/dialog_service.rs diff --git a/webui/src/services/event_service.rs b/frontend/src/services/event_service.rs similarity index 100% rename from webui/src/services/event_service.rs rename to frontend/src/services/event_service.rs diff --git a/webui/src/services/mod.rs b/frontend/src/services/mod.rs similarity index 100% rename from webui/src/services/mod.rs rename to frontend/src/services/mod.rs diff --git a/webui/src/services/playlist_service.rs b/frontend/src/services/playlist_service.rs similarity index 100% rename from webui/src/services/playlist_service.rs rename to frontend/src/services/playlist_service.rs diff --git a/webui/src/services/requests.rs b/frontend/src/services/requests.rs similarity index 100% rename from webui/src/services/requests.rs rename to frontend/src/services/requests.rs diff --git a/webui/src/services/status_service.rs b/frontend/src/services/status_service.rs similarity index 100% rename from webui/src/services/status_service.rs rename to frontend/src/services/status_service.rs diff --git a/webui/src/services/toastr_service.rs b/frontend/src/services/toastr_service.rs similarity index 100% rename from webui/src/services/toastr_service.rs rename to frontend/src/services/toastr_service.rs diff --git a/webui/src/services/websocket_service.rs b/frontend/src/services/websocket_service.rs similarity index 100% rename from webui/src/services/websocket_service.rs rename to frontend/src/services/websocket_service.rs diff --git a/frontend/src/utils/click-away-listener.tsx b/frontend/src/utils/click-away-listener.tsx deleted file mode 100644 index 357492420..000000000 --- a/frontend/src/utils/click-away-listener.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import * as React from 'react'; -import { - useRef, - useEffect, - RefCallback, - cloneElement, - ReactElement, - HTMLAttributes, - MutableRefObject, - FunctionComponent -} from 'react'; - -type MouseEvents = 'click' | 'mousedown' | 'mouseup'; -type TouchEvents = 'touchstart' | 'touchend'; -type Events = MouseEvent | TouchEvent | KeyboardEvent; - -interface Props extends HTMLAttributes { - onClickAway: (event: Events) => void; - mouseEvent?: MouseEvents; - touchEvent?: TouchEvents; - children: ReactElement; -} - -const eventTypeMapping = { - click: 'onClick', - mousedown: 'onMouseDown', - mouseup: 'onMouseUp', - touchstart: 'onTouchStart', - touchend: 'onTouchEnd' -}; - -const ClickAwayListener: FunctionComponent = ({ - children, - onClickAway, - mouseEvent = 'click', - touchEvent = 'touchend' - }) => { - const node = useRef(undefined); - const bubbledEventTarget = useRef(undefined); - const mountedRef = useRef(false); - - /** - * Prevents the bubbled event from getting triggered immediately - * https://github.com/facebook/react/issues/20074 - */ - useEffect(() => { - setTimeout(() => { - mountedRef.current = true; - }, 0); - - return () => { - mountedRef.current = false; - }; - }, []); - - const handleBubbledEvents = - (type: string) => - (event: Events): void => { - bubbledEventTarget.current = event.target; - - const handler = children?.props[type]; - - if (handler) { - handler(event); - } - }; - - const handleChildRef = (childRef: HTMLElement) => { - node.current = childRef; - - let {ref} = children as typeof children & { - ref: RefCallback | MutableRefObject; - }; - - if (typeof ref === 'function') { - ref(childRef); - } else if (ref) { - ref.current = childRef; - } - }; - - useEffect(() => { - const handleEvents = (event: Events): void => { - if (!mountedRef.current) return; - - if ( - (node.current && node.current.contains(event.target as Node)) || - bubbledEventTarget.current === event.target || - !document.contains(event.target as Node) - ) { - return; - } - - onClickAway(event); - }; - - const handleKeayboardEvent = (evt: KeyboardEvent) => { - if (!mountedRef.current) return; - if (evt.key === 'Escape') { - onClickAway(evt); - } - } - - document.addEventListener(mouseEvent, handleEvents); - document.addEventListener(touchEvent, handleEvents); - document.addEventListener('keydown', handleKeayboardEvent, {passive: true}); - - return () => { - document.removeEventListener(mouseEvent, handleEvents); - document.removeEventListener(touchEvent, handleEvents); - document.removeEventListener('keydown', handleKeayboardEvent); - }; - }, [mouseEvent, onClickAway, touchEvent]); - - const mappedMouseEvent = eventTypeMapping[mouseEvent]; - const mappedTouchEvent = eventTypeMapping[touchEvent]; - - return React.Children.only( - // @ts-ignore - cloneElement(children as ReactElement, { - ref: handleChildRef, - [mappedMouseEvent]: handleBubbledEvents(mappedMouseEvent), - [mappedTouchEvent]: handleBubbledEvents(mappedTouchEvent) - }) - ); -}; - -ClickAwayListener.displayName = 'ClickAwayListener'; - -export default ClickAwayListener; diff --git a/frontend/src/utils/clipboard.ts b/frontend/src/utils/clipboard.ts deleted file mode 100644 index b540d0256..000000000 --- a/frontend/src/utils/clipboard.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {Observable, of, from} from "rxjs"; -function copyToClipboardOld(text: string): boolean { - const el: HTMLElement = document.createElement('span'); - el.innerText = text; - el.style.position = 'absolute'; - el.style.border = '0'; - el.style.padding = '0'; - el.style.margin = '0'; - // Move element out of screen horizontally - el.style.position = 'absolute'; - const isRTL = document.documentElement.getAttribute('dir') === 'rtl'; - el.style[ isRTL ? 'right' : 'left' ] = '-9999px'; - // Move element to the same position vertically - const yPosition = window.pageYOffset || document.documentElement.scrollTop; - el.style.top = `${yPosition}px`; - document.body.appendChild(el); - - const selection = window.getSelection(); - const range = document.createRange(); - range.selectNode(el); - selection.removeAllRanges(); - selection.addRange(range); - - let result; - try { - result = document.execCommand('copy'); - selection.removeAllRanges(); - - } catch(err) { - result = false; - } - document.body.removeChild(el) - return result; -} - -export default function copyToClipboard(text: string): Observable { - let result: Observable; - if (copyToClipboardOld(text)) { - result = of(true); - } else { - const promise = navigator.clipboard.writeText(text); - result = new Observable(observer => { - from(promise).subscribe({ - next: () => observer.next(true), - error: (err) => observer.next(false), - complete: () => observer.complete() - }) - }); - } - return result; -} diff --git a/frontend/src/utils/config-utils.ts b/frontend/src/utils/config-utils.ts deleted file mode 100644 index 20a52033f..000000000 --- a/frontend/src/utils/config-utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import ServerConfig from "../model/server-config"; - -const getTargetNames = (config: ServerConfig): string[] => { - return config?.sources.flatMap(s => s.targets) - .map(t => t.name).filter(n => "default" !== n) || []; -} - - -const ConfigUtils = { - getTargetNames -} - -export default ConfigUtils; \ No newline at end of file diff --git a/frontend/src/utils/date-utils.ts b/frontend/src/utils/date-utils.ts deleted file mode 100644 index 26ca53d72..000000000 --- a/frontend/src/utils/date-utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -function formatDate(date: any): string { - if (typeof date === 'string') { - return date; - } - if (typeof date === 'number') { - date = new Date(date); - - } - if (typeof date?.getTime === 'function') { - const day = String(date.getDate()).padStart(2, '0'); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const year = date.getFullYear(); - - return `${year}-${month}-${day}`; - } - return ''; -} - -const DateUtils = { - formatDate: (date: any) => { - if (date) { - return formatDate(date); - } - return ''; - }, - toUnixSeconds: (date: any) => { - if (date && typeof date.getTime == 'function') { - if (date.getTime() > 0) { - return Math.floor(date.getTime() / 1000); - } - return 0; - } - if (date && typeof date == 'number') { - if ( date > 0) { - return Math.floor(date / 1000); - } - return 0; - } - return date; - }, - unixSecondsToDate: (date: any) => { - if (date && typeof date == 'number') { - return new Date(date * 1000); - } - if (date && typeof date.getTime == 'function') { - return date; - } - return date; - } -} - -export default DateUtils; \ No newline at end of file diff --git a/frontend/src/utils/fetcher.ts b/frontend/src/utils/fetcher.ts deleted file mode 100644 index 022e3f8de..000000000 --- a/frontend/src/utils/fetcher.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {first, Observable} from "rxjs"; - -const fetchJson = (fileName: string): Observable => { - return new Observable((observer) => { - fetch(process.env.PUBLIC_URL + fileName, {method: 'GET'}) - .then(res => res.json()) - .then(data => { - observer.next(data) - observer.complete(); - }) - .catch((e) => observer.error(e)); - }).pipe(first()); -} - -const Fetcher = { - fetchJson, -} - -export default Fetcher; diff --git a/frontend/src/utils/i18n.ts b/frontend/src/utils/i18n.ts deleted file mode 100644 index ee962c202..000000000 --- a/frontend/src/utils/i18n.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {Observable, zip} from "rxjs"; -import i18next from "i18next"; -import {initReactI18next} from "react-i18next"; -import Fetcher from "./fetcher"; -import {LANGUAGE, LANGUAGE_LOCAL_STORAGE_KEY} from "../hook/use-translator"; - -export default function i18n_init(languages: string[]): Observable { - return new Observable(observer => { - const failed = (err: any) => { - console.error('failed to load i18n'); - observer.error(err); - } - - i18next.use(initReactI18next) - zip(languages.map(lang => Fetcher.fetchJson('assets/i18n/' + lang + '_common.json'))) - .subscribe({ - next: (responses) => { - let savedLanguage = localStorage.getItem(LANGUAGE_LOCAL_STORAGE_KEY); - if (savedLanguage?.length ) { - LANGUAGE.next(savedLanguage); - } else { - LANGUAGE.next(languages[0]); - } - // 'common' is our custom namespace - const resources = languages.reduce((acc: any, lang, idx) => { - acc[lang] = {common: responses[idx]}; - return acc; - }, {}); - i18next.init({ - interpolation: {escapeValue: false}, // React already does escaping - lng: savedLanguage, // language to use - fallbackLng: {default: ['en']}, - resources, - }).then(() => observer.next(true)) - .catch(failed); - }, - error: failed - }); - }); -} \ No newline at end of file diff --git a/frontend/src/utils/icon-utils.tsx b/frontend/src/utils/icon-utils.tsx deleted file mode 100644 index 7446eb461..000000000 --- a/frontend/src/utils/icon-utils.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from "react"; -import SvgIcon from "../component/svg-icon/svg-icon"; - -export default function createIcon(name: string, path: string): React.JSX.Element { - return ; -} diff --git a/webui/src/utils/mod.rs b/frontend/src/utils/mod.rs similarity index 100% rename from webui/src/utils/mod.rs rename to frontend/src/utils/mod.rs diff --git a/webui/src/utils/storage.rs b/frontend/src/utils/storage.rs similarity index 100% rename from webui/src/utils/storage.rs rename to frontend/src/utils/storage.rs diff --git a/frontend/src/utils/text-generator.ts b/frontend/src/utils/text-generator.ts deleted file mode 100644 index 37ec4c411..000000000 --- a/frontend/src/utils/text-generator.ts +++ /dev/null @@ -1,1162 +0,0 @@ -const NAMES = [ - 'Aaron', - 'Adam', - 'Adrian', - 'Adriano', - 'Ahmad', - 'Ahmed', - 'Ahmet', - 'Alan', - 'Albert', - 'Alessandro', - 'Alessio', - 'Alex', - 'Alexander', - 'Alfred', - 'Ali', - 'Amar', - 'Amir', - 'Amon', - 'Andre', - 'Andreas', - 'Andrew', - 'Angelo', - 'Ansgar', - 'Anthony', - 'Anton', - 'Antonio', - 'Arda', - 'Arian', - 'Armin', - 'Arne', - 'Arno', - 'Arthur', - 'Artur', - 'Arved', - 'Arvid', - 'Ayman', - 'Baran', - 'Baris', - 'Bastian', - 'Batuhan', - 'Bela', - 'Ben', - 'Benedikt', - 'Benjamin', - 'Bennet', - 'Bennett', - 'Benno', - 'Bent', - 'Berat', - 'Berkay', - 'Bernd', - 'Bilal', - 'Bjarne', - 'Björn', - 'Bo', - 'Boris', - 'Brandon', - 'Brian', - 'Bruno', - 'Bryan', - 'Burak', - 'Calvin', - 'Can', - 'Carl', - 'Carlo', - 'Carlos', - 'Caspar', - 'Cedric', - 'Cedrik', - 'Cem', - 'Charlie', - 'Chris', - 'Christian', - 'Christiano', - 'Christoph', - 'Christopher', - 'Claas', - 'Clemens', - 'Colin', - 'Collin', - 'Conner', - 'Connor', - 'Constantin', - 'Corvin', - 'Curt', - 'Damian', - 'Damien', - 'Daniel', - 'Danilo', - 'Danny', - 'Darian', - 'Dario', - 'Darius', - 'Darren', - 'David', - 'Davide', - 'Davin', - 'Dean', - 'Deniz', - 'Dennis', - 'Denny', - 'Devin', - 'Diego', - 'Dion', - 'Domenic', - 'Domenik', - 'Dominic', - 'Dominik', - 'Dorian', - 'Dustin', - 'Dylan', - 'Ecrin', - 'Eddi', - 'Eddy', - 'Edgar', - 'Edwin', - 'Efe', - 'Ege', - 'Elia', - 'Eliah', - 'Elias', - 'Elijah', - 'Emanuel', - 'Emil', - 'Emilian', - 'Emilio', - 'Emir', - 'Emirhan', - 'Emre', - 'Enes', - 'Enno', - 'Enrico', - 'Eren', - 'Eric', - 'Erik', - 'Etienne', - 'Fabian', - 'Fabien', - 'Fabio', - 'Fabrice', - 'Falk', - 'Felix', - 'Ferdinand', - 'Fiete', - 'Filip', - 'Finlay', - 'Finley', - 'Finn', - 'Finnley', - 'Florian', - 'Francesco', - 'Franz', - 'Frederic', - 'Frederick', - 'Frederik', - 'Friedrich', - 'Fritz', - 'Furkan', - 'Fynn', - 'Gabriel', - 'Georg', - 'Gerrit', - 'Gian', - 'Gianluca', - 'Gino', - 'Giuliano', - 'Giuseppe', - 'Gregor', - 'Gustav', - 'Hagen', - 'Hamza', - 'Hannes', - 'Hanno', - 'Hans', - 'Hasan', - 'Hassan', - 'Hauke', - 'Hendrik', - 'Hennes', - 'Henning', - 'Henri', - 'Henrick', - 'Henrik', - 'Henry', - 'Hugo', - 'Hussein', - 'Ian', - 'Ibrahim', - 'Ilias', - 'Ilja', - 'Ilyas', - 'Immanuel', - 'Ismael', - 'Ismail', - 'Ivan', - 'Iven', - 'Jack', - 'Jacob', - 'Jaden', - 'Jakob', - 'Jamal', - 'James', - 'Jamie', - 'Jan', - 'Janek', - 'Janis', - 'Janne', - 'Jannek', - 'Jannes', - 'Jannik', - 'Jannis', - 'Jano', - 'Janosch', - 'Jared', - 'Jari', - 'Jarne', - 'Jarno', - 'Jaron', - 'Jason', - 'Jasper', - 'Jay', - 'Jayden', - 'Jayson', - 'Jean', - 'Jens', - 'Jeremias', - 'Jeremie', - 'Jeremy', - 'Jermaine', - 'Jerome', - 'Jesper', - 'Jesse', - 'Jim', - 'Jimmy', - 'Joe', - 'Joel', - 'Joey', - 'Johann', - 'Johannes', - 'John', - 'Johnny', - 'Jon', - 'Jona', - 'Jonah', - 'Jonas', - 'Jonathan', - 'Jonte', - 'Joost', - 'Jordan', - 'Joris', - 'Joscha', - 'Joschua', - 'Josef', - 'Joseph', - 'Josh', - 'Joshua', - 'Josua', - 'Juan', - 'Julian', - 'Julien', - 'Julius', - 'Juri', - 'Justin', - 'Justus', - 'Kaan', - 'Kai', - 'Kalle', - 'Karim', - 'Karl', - 'Karlo', - 'Kay', - 'Keanu', - 'Kenan', - 'Kenny', - 'Keno', - 'Kerem', - 'Kerim', - 'Kevin', - 'Kian', - 'Kilian', - 'Kim', - 'Kimi', - 'Kjell', - 'Klaas', - 'Klemens', - 'Konrad', - 'Konstantin', - 'Koray', - 'Korbinian', - 'Kurt', - 'Lars', - 'Lasse', - 'Laurence', - 'Laurens', - 'Laurenz', - 'Laurin', - 'Lean', - 'Leander', - 'Leandro', - 'Leif', - 'Len', - 'Lenn', - 'Lennard', - 'Lennart', - 'Lennert', - 'Lennie', - 'Lennox', - 'Lenny', - 'Leo', - 'Leon', - 'Leonard', - 'Leonardo', - 'Leonhard', - 'Leonidas', - 'Leopold', - 'Leroy', - 'Levent', - 'Levi', - 'Levin', - 'Lewin', - 'Lewis', - 'Liam', - 'Lian', - 'Lias', - 'Lino', - 'Linus', - 'Lio', - 'Lion', - 'Lionel', - 'Logan', - 'Lorenz', - 'Lorenzo', - 'Loris', - 'Louis', - 'Luan', - 'Luc', - 'Luca', - 'Lucas', - 'Lucian', - 'Lucien', - 'Ludwig', - 'Luis', - 'Luiz', - 'Luk', - 'Luka', - 'Lukas', - 'Luke', - 'Lutz', - 'Maddox', - 'Mads', - 'Magnus', - 'Maik', - 'Maksim', - 'Malik', - 'Malte', - 'Manuel', - 'Marc', - 'Marcel', - 'Marco', - 'Marcus', - 'Marek', - 'Marian', - 'Mario', - 'Marius', - 'Mark', - 'Marko', - 'Markus', - 'Marlo', - 'Marlon', - 'Marten', - 'Martin', - 'Marvin', - 'Marwin', - 'Mateo', - 'Mathis', - 'Matis', - 'Mats', - 'Matteo', - 'Mattes', - 'Matthias', - 'Matthis', - 'Matti', - 'Mattis', - 'Maurice', - 'Max', - 'Maxim', - 'Maximilian', - 'Mehmet', - 'Meik', - 'Melvin', - 'Merlin', - 'Mert', - 'Michael', - 'Michel', - 'Mick', - 'Miguel', - 'Mika', - 'Mikail', - 'Mike', - 'Milan', - 'Milo', - 'Mio', - 'Mirac', - 'Mirco', - 'Mirko', - 'Mohamed', - 'Mohammad', - 'Mohammed', - 'Moritz', - 'Morten', - 'Muhammed', - 'Murat', - 'Mustafa', - 'Nathan', - 'Nathanael', - 'Nelson', - 'Neo', - 'Nevio', - 'Nick', - 'Niclas', - 'Nico', - 'Nicolai', - 'Nicolas', - 'Niels', - 'Nikita', - 'Niklas', - 'Niko', - 'Nikolai', - 'Nikolas', - 'Nils', - 'Nino', - 'Noah', - 'Noel', - 'Norman', - 'Odin', - 'Oke', - 'Ole', - 'Oliver', - 'Omar', - 'Onur', - 'Oscar', - 'Oskar', - 'Pascal', - 'Patrice', - 'Patrick', - 'Paul', - 'Peer', - 'Pepe', - 'Peter', - 'Phil', - 'Philip', - 'Philipp', - 'Pierre', - 'Piet', - 'Pit', - 'Pius', - 'Quentin', - 'Quirin', - 'Rafael', - 'Raik', - 'Ramon', - 'Raphael', - 'Rasmus', - 'Raul', - 'Rayan', - 'René', - 'Ricardo', - 'Riccardo', - 'Richard', - 'Rick', - 'Rico', - 'Robert', - 'Robin', - 'Rocco', - 'Roman', - 'Romeo', - 'Ron', - 'Ruben', - 'Ryan', - 'Said', - 'Salih', - 'Sam', - 'Sami', - 'Sammy', - 'Samuel', - 'Sandro', - 'Santino', - 'Sascha', - 'Sean', - 'Sebastian', - 'Selim', - 'Semih', - 'Shawn', - 'Silas', - 'Simeon', - 'Simon', - 'Sinan', - 'Sky', - 'Stefan', - 'Steffen', - 'Stephan', - 'Steve', - 'Steven', - 'Sven', - 'Sönke', - 'Sören', - 'Taha', - 'Tamino', - 'Tammo', - 'Tarik', - 'Tayler', - 'Taylor', - 'Teo', - 'Theo', - 'Theodor', - 'Thies', - 'Thilo', - 'Thomas', - 'Thorben', - 'Thore', - 'Thorge', - 'Tiago', - 'Til', - 'Till', - 'Tillmann', - 'Tim', - 'Timm', - 'Timo', - 'Timon', - 'Timothy', - 'Tino', - 'Titus', - 'Tizian', - 'Tjark', - 'Tobias', - 'Tom', - 'Tommy', - 'Toni', - 'Tony', - 'Torben', - 'Tore', - 'Tristan', - 'Tyler', - 'Tyron', - 'Umut', - 'Valentin', - 'Valentino', - 'Veit', - 'Victor', - 'Viktor', - 'Vin', - 'Vincent', - 'Vito', - 'Vitus', - 'Wilhelm', - 'Willi', - 'William', - 'Willy', - 'Xaver', - 'Yannic', - 'Yannick', - 'Yannik', - 'Yannis', - 'Yasin', - 'Youssef', - 'Yunus', - 'Yusuf', - 'Yven', - 'Yves', - 'Ömer', - 'Aaliyah', - 'Abby', - 'Abigail', - 'Ada', - 'Adelina', - 'Adriana', - 'Aileen', - 'Aimee', - 'Alana', - 'Alea', - 'Alena', - 'Alessa', - 'Alessia', - 'Alexa', - 'Alexandra', - 'Alexia', - 'Alexis', - 'Aleyna', - 'Alia', - 'Alica', - 'Alice', - 'Alicia', - 'Alina', - 'Alisa', - 'Alisha', - 'Alissa', - 'Aliya', - 'Aliyah', - 'Allegra', - 'Alma', - 'Alyssa', - 'Amalia', - 'Amanda', - 'Amelia', - 'Amelie', - 'Amina', - 'Amira', - 'Amy', - 'Ana', - 'Anabel', - 'Anastasia', - 'Andrea', - 'Angela', - 'Angelina', - 'Angelique', - 'Anja', - 'Ann', - 'Anna', - 'Annabel', - 'Annabell', - 'Annabelle', - 'Annalena', - 'Anne', - 'Anneke', - 'Annelie', - 'Annemarie', - 'Anni', - 'Annie', - 'Annika', - 'Anny', - 'Anouk', - 'Antonia', - 'Ariana', - 'Ariane', - 'Arwen', - 'Ashley', - 'Asya', - 'Aurelia', - 'Aurora', - 'Ava', - 'Ayleen', - 'Aylin', - 'Ayse', - 'Azra', - 'Betty', - 'Bianca', - 'Bianka', - 'Caitlin', - 'Cara', - 'Carina', - 'Carla', - 'Carlotta', - 'Carmen', - 'Carolin', - 'Carolina', - 'Caroline', - 'Cassandra', - 'Catharina', - 'Catrin', - 'Cecile', - 'Cecilia', - 'Celia', - 'Celina', - 'Celine', - 'Ceyda', - 'Ceylin', - 'Chantal', - 'Charleen', - 'Charlotta', - 'Charlotte', - 'Chayenne', - 'Cheyenne', - 'Chiara', - 'Christin', - 'Christina', - 'Cindy', - 'Claire', - 'Clara', - 'Clarissa', - 'Colleen', - 'Collien', - 'Cora', - 'Corinna', - 'Cosima', - 'Dana', - 'Daniela', - 'Daria', - 'Darleen', - 'Defne', - 'Delia', - 'Denise', - 'Diana', - 'Dilara', - 'Dina', - 'Dorothea', - 'Eda', - 'Eileen', - 'Ela', - 'Elaine', - 'Elanur', - 'Elea', - 'Elena', - 'Eleni', - 'Eleonora', - 'Eliana', - 'Elif', - 'Elina', - 'Elisa', - 'Elisabeth', - 'Ella', - 'Ellen', - 'Elli', - 'Elly', - 'Elsa', - 'Emelie', - 'Emely', - 'Emilia', - 'Emilie', - 'Emily', - 'Emma', - 'Emmely', - 'Emmi', - 'Emmy', - 'Enie', - 'Enna', - 'Enya', - 'Esma', - 'Estelle', - 'Esther', - 'Eva', - 'Evelin', - 'Evelina', - 'Eveline', - 'Evelyn', - 'Fabienne', - 'Fatima', - 'Fatma', - 'Felicia', - 'Felicitas', - 'Felina', - 'Femke', - 'Fenja', - 'Fine', - 'Finia', - 'Finja', - 'Finnja', - 'Fiona', - 'Flora', - 'Florentine', - 'Francesca', - 'Franka', - 'Franziska', - 'Frederike', - 'Freya', - 'Frida', - 'Frieda', - 'Friederike', - 'Giada', - 'Gina', - 'Giulia', - 'Giuliana', - 'Greta', - 'Hailey', - 'Hana', - 'Hanna', - 'Hannah', - 'Heidi', - 'Helen', - 'Helena', - 'Helene', - 'Helin', - 'Henriette', - 'Henrike', - 'Hermine', - 'Ida', - 'Ilayda', - 'Imke', - 'Ina', - 'Ines', - 'Inga', - 'Inka', - 'Irem', - 'Isa', - 'Isabel', - 'Isabell', - 'Isabella', - 'Isabelle', - 'Ivonne', - 'Jacqueline', - 'Jamila', - 'Jana', - 'Jane', - 'Janin', - 'Janina', - 'Janine', - 'Janna', - 'Jara', - 'Jasmin', - 'Jasmina', - 'Jasmine', - 'Jella', - 'Jenna', - 'Jennifer', - 'Jenny', - 'Jessica', - 'Jessy', - 'Jette', - 'Jil', - 'Jill', - 'Joana', - 'Joanna', - 'Joelina', - 'Joeline', - 'Joelle', - 'Johanna', - 'Joleen', - 'Jolie', - 'Jolien', - 'Jolin', - 'Jolina', - 'Joline', - 'Jonna', - 'Josefin', - 'Josefine', - 'Josephin', - 'Josephine', - 'Josie', - 'Josy', - 'Joy', - 'Joyce', - 'Judith', - 'Judy', - 'Jule', - 'Julia', - 'Juliana', - 'Juliane', - 'Julie', - 'Julienne', - 'Julika', - 'Julina', - 'Juna', - 'Justine', - 'Kaja', - 'Karina', - 'Karla', - 'Karlotta', - 'Karolina', - 'Karoline', - 'Kassandra', - 'Katarina', - 'Katharina', - 'Kathrin', - 'Katja', - 'Katrin', - 'Kaya', - 'Kayra', - 'Kiana', - 'Kiara', - 'Kimberley', - 'Kimberly', - 'Kira', - 'Klara', - 'Korinna', - 'Kristin', - 'Kyra', - 'Laila', - 'Lana', - 'Lara', - 'Larissa', - 'Laura', - 'Laureen', - 'Lavinia', - 'Lea', - 'Leah', - 'Leana', - 'Leandra', - 'Leann', - 'Lee', - 'Leila', - 'Lena', - 'Lene', - 'Leni', - 'Lenia', - 'Lenja', - 'Lenya', - 'Leona', - 'Leoni', - 'Leonie', - 'Leonora', - 'Leticia', - 'Letizia', - 'Levke', - 'Leyla', - 'Lia', - 'Liah', - 'Liana', - 'Lili', - 'Lilia', - 'Lilian', - 'Liliana', - 'Lilith', - 'Lilli', - 'Lillian', - 'Lilly', - 'Lily', - 'Lina', - 'Linda', - 'Lindsay', - 'Line', - 'Linn', - 'Linnea', - 'Lisa', - 'Lisann', - 'Lisanne', - 'Liv', - 'Livia', - 'Liz', - 'Lola', - 'Loreen', - 'Lorena', - 'Lotta', - 'Lotte', - 'Louisa', - 'Louise', - 'Luana', - 'Lucia', - 'Lucie', - 'Lucienne', - 'Lucy', - 'Luisa', - 'Luise', - 'Luna', - 'Luzie', - 'Lya', - 'Lydia', - 'Lyn', - 'Lynn', - 'Madeleine', - 'Madita', - 'Madleen', - 'Madlen', - 'Magdalena', - 'Maike', - 'Mailin', - 'Maira', - 'Maja', - 'Malena', - 'Malia', - 'Malin', - 'Malina', - 'Mandy', - 'Mara', - 'Marah', - 'Mareike', - 'Maren', - 'Maria', - 'Mariam', - 'Marie', - 'Marieke', - 'Mariella', - 'Marika', - 'Marina', - 'Marisa', - 'Marissa', - 'Marit', - 'Marla', - 'Marleen', - 'Marlen', - 'Marlena', - 'Marlene', - 'Marta', - 'Martha', - 'Mary', - 'Maryam', - 'Mathilda', - 'Mathilde', - 'Matilda', - 'Maxi', - 'Maxima', - 'Maxine', - 'Maya', - 'Mayra', - 'Medina', - 'Medine', - 'Meike', - 'Melanie', - 'Melek', - 'Melike', - 'Melina', - 'Melinda', - 'Melis', - 'Melisa', - 'Melissa', - 'Merle', - 'Merve', - 'Meryem', - 'Mette', - 'Mia', - 'Michaela', - 'Michelle', - 'Mieke', - 'Mila', - 'Milana', - 'Milena', - 'Milla', - 'Mina', - 'Mira', - 'Miray', - 'Miriam', - 'Mirja', - 'Mona', - 'Monique', - 'Nadine', - 'Nadja', - 'Naemi', - 'Nancy', - 'Naomi', - 'Natalia', - 'Natalie', - 'Nathalie', - 'Neele', - 'Nela', - 'Nele', - 'Nelli', - 'Nelly', - 'Nia', - 'Nicole', - 'Nika', - 'Nike', - 'Nila', - 'Nina', - 'Nisa', - 'Noemi', - 'Nora', - 'Olivia', - 'Patricia', - 'Patrizia', - 'Paula', - 'Paulina', - 'Pauline', - 'Penelope', - 'Philine', - 'Phoebe', - 'Pia', - 'Rahel', - 'Rania', - 'Rebecca', - 'Rebekka', - 'Riana', - 'Rieke', - 'Rike', - 'Romina', - 'Romy', - 'Ronja', - 'Rosa', - 'Rosalie', - 'Ruby', - 'Sabrina', - 'Sahra', - 'Sally', - 'Salome', - 'Samantha', - 'Samia', - 'Samira', - 'Sandra', - 'Sandy', - 'Sanja', - 'Saphira', - 'Sara', - 'Sarah', - 'Saskia', - 'Selin', - 'Selina', - 'Selma', - 'Sena', - 'Sidney', - 'Sienna', - 'Silja', - 'Sina', - 'Sinja', - 'Smilla', - 'Sofia', - 'Sofie', - 'Sonja', - 'Sophia', - 'Sophie', - 'Soraya', - 'Stefanie', - 'Stella', - 'Stephanie', - 'Stina', - 'Sude', - 'Summer', - 'Susanne', - 'Svea', - 'Svenja', - 'Sydney', - 'Tabea', - 'Talea', - 'Talia', - 'Tamara', - 'Tamia', - 'Tamina', - 'Tanja', - 'Tara', - 'Tarja', - 'Teresa', - 'Tessa', - 'Thalea', - 'Thalia', - 'Thea', - 'Theresa', - 'Tia', - 'Tina', - 'Tomke', - 'Tuana', - 'Valentina', - 'Valeria', - 'Valerie', - 'Vanessa', - 'Vera', - 'Veronika', - 'Victoria', - 'Viktoria', - 'Viola', - 'Vivian', - 'Vivien', - 'Vivienne', - 'Wibke', - 'Wiebke', - 'Xenia', - 'Yara', - 'Yaren', - 'Yasmin', - 'Yvonne', - 'Zara', - 'Zehra', - 'Zeynep', - 'Zoe', - 'Zoey', -]; - - -const generateUsername = (): string => { - const idx = Math.floor(Math.random() * NAMES.length); - return NAMES[idx] + Math.floor(Math.random() * 100); -} - - - -const generatePassword = (length: number = 12): string => { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - let retVal = ""; - for (let i = 0, n = charset.length; i < length; i++) { - retVal += charset.charAt(Math.floor(Math.random() * n)); - } - return retVal; -} - -const TextGenerator = {generatePassword, generateUsername}; -export default TextGenerator; \ No newline at end of file diff --git a/frontend/src/utils/uuid.ts b/frontend/src/utils/uuid.ts deleted file mode 100644 index 637619e27..000000000 --- a/frontend/src/utils/uuid.ts +++ /dev/null @@ -1,17 +0,0 @@ -// yes, i know this is a fake this is not a real uuid -export const genUuid = () => { - let d = new Date().getTime();//Timestamp - // Time in microseconds since page-load or 0 if unsupported - let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0; - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - let r = Math.random() * 16;//random number between 0 and 16 - if(d > 0){//Use timestamp until depleted - r = (d + r)%16 | 0; - d = Math.floor(d/16); - } else {//Use microseconds since page-load if supported - r = (d2 + r)%16 | 0; - d2 = Math.floor(d2/16); - } - return (c === 'x' ? r : ((r & 0x3) | 0x8)).toString(16); - }); -} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index 5d8956e36..000000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "noFallthroughCasesInSwitch": true, - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "strictNullChecks": false, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": [ - "src" - ] -} diff --git a/webui/public/assets/favicon.ico b/webui/public/assets/favicon.ico deleted file mode 100644 index 8d861c4c85eb5293ffa3a9e614604401ffbfc07a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 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 NrlL1TjT(j_003pUjt~F< diff --git a/webui/public/assets/fonts.css b/webui/public/assets/fonts.css deleted file mode 100644 index 01d5d09a4..000000000 --- a/webui/public/assets/fonts.css +++ /dev/null @@ -1,59 +0,0 @@ -/* rubik-arabic-400-normal */ -@font-face { - font-family: 'Rubik'; - font-style: normal; - font-display: swap; - font-weight: 400; - src: url('fonts/rubik-arabic-400-normal.woff2') format('woff2'), url('fonts/rubik-arabic-400-normal.woff') format('woff'); - unicode-range: U+0600-06FF,U+0750-077F,U+0870-088E,U+0890-0891,U+0897-08E1,U+08E3-08FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1; -} - -/* rubik-cyrillic-ext-400-normal */ -@font-face { - font-family: 'Rubik'; - font-style: normal; - font-display: swap; - font-weight: 400; - src: url('fonts/rubik-cyrillic-ext-400-normal.woff2') format('woff2'), url('fonts/rubik-cyrillic-ext-400-normal.woff') format('woff'); - unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; -} - -/* rubik-cyrillic-400-normal */ -@font-face { - font-family: 'Rubik'; - font-style: normal; - font-display: swap; - font-weight: 400; - src: url('fonts/rubik-cyrillic-400-normal.woff2') format('woff2'), url('fonts/rubik-cyrillic-400-normal.woff') format('woff'); - unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; -} - -/* rubik-hebrew-400-normal */ -@font-face { - font-family: 'Rubik'; - font-style: normal; - font-display: swap; - font-weight: 400; - src: url('fonts/rubik-hebrew-400-normal.woff2') format('woff2'), url('fonts/rubik-hebrew-400-normal.woff') format('woff'); - unicode-range: U+0307-0308,U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F; -} - -/* rubik-latin-ext-400-normal */ -@font-face { - font-family: 'Rubik'; - font-style: normal; - font-display: swap; - font-weight: 400; - src: url('fonts/rubik-latin-ext-400-normal.woff2') format('woff2'), url('fonts/rubik-latin-ext-400-normal.woff') format('woff'); - unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; -} - -/* rubik-latin-400-normal */ -@font-face { - font-family: 'Rubik'; - font-style: normal; - font-display: swap; - font-weight: 400; - src: url('fonts/rubik-latin-400-normal.woff2') format('woff2'), url('fonts/rubik-latin-400-normal.woff') format('woff'); - unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; -} diff --git a/webui/public/assets/fonts/rubik-arabic-400-italic.woff b/webui/public/assets/fonts/rubik-arabic-400-italic.woff deleted file mode 100644 index fca1c231b73f4bf03547c69e0ed53fe704a703b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22396 zcmXT-cXMN4WME)m2(Mw_2hrbZ7#JZUARaPycX4%NU|@`3U|^_VU|`rGV#dG7J-|Ph zfr0S?0|SRA0|UR&9{t3=CW{3=B-O85mfsWMKx#o)AU&rtje&tbf`P$SkAdN6$CX#tr(~oirZ6!0bTBY5n1Qfb&K~cKjMPL1 z2A=~A3=GN)3=Aq07<`^EFffTTFtEJH5K&Xf$xlvXVDS6Hz`!&Mgqa>+ z*3M0=C}3a+oWa1rAj81GpuiZ{mg4aR~!M&=m#- zmK6*PybsiG`de4r((T&X$mAfxn*J@Ov4_c{uv}=5&=R3j2Rb`e^8NqC!}*7qH^K1% zbEW&fAFfG06D_q<4o^F9kiRLjeUHHN*&?b@Un>6nW{Z25X;Gk3u?&F_81 z@^)YD?R7y*J427fxol|A(TFWdRzLdi@LrCJ1EPMypE~1RCc22JRvg_Knkq6SEle~x zJSry2NAlqG{Q4w~lWR_Gk-pg*syapFNm^)N(4@#L~L1$A-@U-uo8xJh_X|YxDQ-@>ECvL-Z&s|O| z=VT6WYYFg8o8D_ampwbq!(Wi)n^FhIqk=VaU(CGsn0o9+Wdro=p3f{PZ4vB^W5sr`rZBD z%U;DiQ+vr2ewWltiDT<;f46&Io$l`D=4Lc$;uEh)6MIrpQ&l5Ro{%^xp?va$hnw3Z zmB^D>?rxJ*JSW9?PFiE;IZ4ISXq}4Zq$R#@i~a6)=U$I^{ITZ!`@h9@$IH)`$L?HR zlz%5k=hD}8C%5j>yZ)N(&WhPZ#cS5>mfil|@6J*S&nfDY^b#xeTx}k5RqDQ9;y7z% z*{a#E7PqY^%$gOoAZ&$iR=?Gfb1SlUwgq~IXc^T!U%+rwut5h)Z2*pS32lk!{b@6y{l50=oB0+Ar+2F8iPU=BW7nW{J*fncx`*Yi#s_ zv_o{(1xJVIyi@9Oo^#}}+%P%!D{yBByxJ8elPm)yew}XCf7su9@|6{+l!6!e7o&CqJ{hyh;A4sHstdKnQ z&s^KY!TyR$?IQoLH}~3j7uVQd7oYe3seM-Gd!w}tF18ze4rZ!lf^!&3nC$_3=;6&)VFxA{W_i6FI%X z#46|KG;i^!jem2VhGni_bL?JZtwMrsN%foW9nYQ3vV_Gp>b>50{+j&x^xVT@FI20p znmOrxQJ5m`#cO$+Z)v-cb3apKfKmAkZrcri16Iiv-sj%kclPM7Kl`J&>>mGm?z{B3 z`Tpu`rax7C_n#|><%z4Pv9A9-J^sIM&lHIeNXF0HJj^JSVDpNeIDe977nvU$% zCCM&2r;99}*VgZkn6b6iCvs)#rfdC*B2}@m#~aH1mXvwBO|uNzyiLhh-|~84wXnzD zT?+&ot}MN5r5;{a9-&t<_sTN&s#U9(@V}V2B5%^Y*VEo*O$k~4_E+FEPt$w%jQp!= zfA>yU6`(u$mTT40!&b{=m&^~)`dhc%rTWX}Z<9|2>dam7Ww-04rl3pQPdpZ1nN%b= z&1$7u>LQ1%3BFaQ-g$p~c4>lQs)wSga%h+8{4Xq#9bR%T4G+y)-tWOb;oV&(p$`Jx zlYER+vW)zg9e1{U&J)OHR7pG%yP@ppa{UdCR!=YQnYbj`(s}u9wYeuOw+Sk~Jw36! z+mpTMjo2NH%;aqi9=+j4Jjd%NVr?T*~rAZG6EUB3R|TctiNsS-=KNb43@DJm@iMt+{Xmb8jU%SyNdV7N3kpp=f z?SJt5Jd=@dT`Y0-u11lqWzc-dTIB+b+POPr((CQNpIf><_5WNow)c{%Wfctn=Eb$0 z-Snm=EofHH;gn6j99=*4{k~Z3W%;dbqP3%8U+?l9lk1^xo+RDt{OR}p;Nmy`&(?_S z6}0VDJFFS~wo=7(eu2Gh|IU;&byi#M=kG7(d~ki{7&v?V)|)S1-#);-(el!ArpFg2 ze-Mk`q0Kt`_Eyy$Q~&M1#$+vVSLiO&Jfp0B1+`DMHigsnHReB@bNU${-@R>pyN_C4 zt(6O?ObRSEpElR?kKW@OE00`l`y&66_r*>VtB7;^nYA-aHcZ?eyewPfga6_wUtX27t(URcTfXLgKfCXz_P6^J__y-yW!P~|+%Y}q2gg2l<;LeJ z(iXkNX$9}=qxjyW=q>%B;$7>nviWSRyP9N8+y?FanfnX)KFQ6w;~Lf2|Apg6%jH)Y z?gfU+ZBzF3SNh)NSeINUWbF9t@RkX4Q`a@NH@D7ZdHV9+EXS&P)|@AA*G&I)My>dL z#997x-{nrGS4;o$3U5v?;hD7FN@{nJzMw#_DqCAV$Q-m@2%&tk4r zxwp6Td}m5;p5vrrLUTR!4@vKRFk4dWUyF)v9#_$I_1&=(4zDmTv6J}q`hn)XFUIA2 zzq4NSOp!}jYJOqI1OI(nYX!^hWm&!HI=eY6+az1*V9LCpbH9R){Ac0&9D1N{n|a4F z;e!!xc(|7*915S?uta~A;iCgm? zd`;Gw=hl0RCE)9J-{>Ev3(C0ni^WN8%e=IM_3YYfy0ad(a|_rCDoFi0^lM+i&h>j% ztz=|mII;Y4*fzdsaos{utpH2A)YDV6b9JJ3ZP=K!)5uLl-*2%z-;)`6f7V?%5ji7( zYkgDKx*c9?rg*Jib879TE*|^32S*a?rpuN$>p$IgS?`Irb;#sX6&#)!hlJ*JxTvZX zdU^x}O`6t|qH1k<(l_o?@Qg=Omf1{Q>6iQJ@{DI&4$j|JvVhz6#bJgvgPy>iL>b{3 zM@-TT=O5KcwVWMsy2@-*uk4#tdv?n+fqli3F3)*tBfj%Y=(^&`x9>c){W%U(xlU~7Zxs@COtiS zZQ)w8$Jf%{Ua6D4n%VR>NW3(*{A+~1;KvRJktBh378OO-%o7fFT!B47Qx*$dUEyjg zmH3WH?CndR@~X@87y6`V84FG=6mAM=QM^y(pO(S&Xr$1k^jt*>J@o-(l~P3Li~ z@|k2o?}%eu*EaO==A>Fz-#E_5SJ;v~=YdGN#c@`>!q)6N55&rM9A}pM*c86!q4NBv zliGh;eAsY9V$#xM7j(?qQWqC8&F*yc^KZzf!iOuRmEqx#+up7y$L zCSvRiOojq`YA9(pc?$#-S?E9u^?0qJMLU$s;% z)_eHo|DNLu8}C1inOreRZo#XM`30{}tSyu{ku`Tt`WcUnXQj6ud&w(Z8h8KR5gUez zTgeHDr;_I!IB@E~fh$KvKK^HJHt$ILkkpfGkhCSifsyMCuZN9&y?uC_8{6^y-_D%= zzuC}WLxy2oP(s3wa|akrx|=ikJ(!t|87{i>ZDFXm#d>ffsN2DE;P>(c=1oUKJf;X` zOPnZp=i<}ly7AR!`SOB$x2JYpOmZ-OzEoam#RukH_gQrrDsJWUI?s@E6mW?D`vP3O zfb+?=+}oD#RL$0Hj{WSbcCBps*9Haw7NOr^`+f!}Fum}sJoIDBNMZby)@`2GX0q(q^&?RM-C2=q*L>aD z{p}K?m5=+&l)1|`m(0_cJ`IZ+*{eM9j}Mwmy}9c5`>l zwAGHi3qP7n6uY1l<))o!`*zLUE&Y7g1g$)mzw(*8n7d?SlzHFmGiM)8`)c!kPxwLM z*%#W@Fu1&9NuRp;jVDEspH;)j&?}&;C}fe;))nr`vo3a>-LhDpS@N{YGNak8I;lIS zObH5#I^_|mss427t}7Fmf&xByhs_stHk#0IZ&H875*5>xCp`0t40{B5(nXRb&a#

cr~5-|x9@dhYK0+h%S%qT?dArXHUtJbhj0)wh|iBz1Ij*XXQ?+@5%O zTP!=Dtaa(LJFPq8d=KxNwr=0T{QXUbgO-IRQHFov*J@y-Ltdp zEmP(+HRv18l&lJR|C)gT)OY^>pMk&P)|P741ZDSaxuK?;Q#|iZQr)`as88B4$^PSV zX(dLpeI(51=zLCo_9rzhE$v?InKRuFH`bX(Oxru}rsuc+DJ$093VlAOdiA@ttHcx- zE>^1k(q6-qgi{G}q+Bj>O5zAbUgA#(DGi+QJ_6tk>@qaG=`gdT)-=+GE z<#tu-uAN6}wf=pcDZbiV;KR)I3zsuE%zgjRzHdQ)69Wh1gbwN48(CLhn}&W~QNpzS z@=FQl%bjICA3SAvsY>tWRxs_hU|9_wu^iaO_R7bzN{!`b9`%C&C3AIe$ z-}bPc>wLM@H4gF4=gQ0u3=GWW2Td7dehWVOwyxIP=h1#eiT8i>|0wN|7cuab>c5|{ z^)_3Yb(>Gbs#l*YUv5cMd9>yH>Q!&cysw(*pOq1a;Bfu6$DeP4OLBhL>WYe}XTOZ* zU2>_hDGz({;rJr%Q%i?pdbz17$bH&GjT1uq{Y+-pdd0r)t4lxsVn%j(N!!|wYixf> zoL8EeaML^gv1$tMbiVVCc=rcE>m4CBN zkbq;sYkm{<+6yfgUpB}b`^Zu#^SkrlzlDiky)|>r&R8s<-pCkn`&X;UwrkcpKNx?> z|ItZ}%#Js!wz!n2>&oMB``Z7b>)1=4XWsDNVz9q){?+LR-m2v#_h(0JjpE+^p?%ut zvn#eRF_>4$eB^TTIda1;v2m*8yw4tMCU||aOPnMqSw4T|<*(DG*G-;&p<3mhXrHo* z^O4V!v`!@%bxr69kYAsa(sMLdrn=w}m))MI7K7K)cV-uV)SUO-ylB#SPzHWRqDwJo}r!5Dn zIqKFcu8z6(zHjxZu+aNjT&v{H{Wxr?JX_gAPO)F5BU`7@DQ|i-i-h)pQ}2#Nm|kEr zj<`K{sqX>p7h4C@bV^)T@q;@TipX*{wT>YJbZDA3y0GN*?DWehOU-O4w%Hz7ZP*$Us1wSk3)_h zZ7T$)m&P0+Rpm5#f$cGd4!xuV%2Im@}^7_9%d?yV3x$1KaB*4KX9DC_(J z6JO0;A*)ulS+#_l?k%%eA?LJiR@|(J%UiadRC;F|ny=D$SN(_0p5w1Bf82L7`)~ov zRWlJ^0h!m6j%H0;aEC>I3Ck=+8CLFM4*b6V?!IDyk^*@@ znLqtEwS`7E%~m;g-^RV4|N33t);xc~w41w=>%E_UaQpwFFYo`+d;c!JO<4T@ z(X@Z{%Wu~gdjFSy`7OTWmwZ&c?kbRK&tsL3`mcTZU-b+9huItVKDqI~;;(R+ z{rAP{5BJ`PEBo`2c|*d@_?9@%_XodqJ>GA(f$2o=zk?PLZ0ZMl84gr%Zk5)xu`IkA z;KbRSma%zKrr8-LC-q&~B~585i_)hFR8$%=`v&jjaXx;#Z1b}%Gq+qUcxIYg$CDqk zBh`{QUEW57=W@vky9piYEKyUK81u_arq7j9M>qW*8Ug&xxr?KPPu^3`is=wXv<8{89f zgM;R8cfUN7ov&5dMtsed?U}3i+n;v{woYzc=pu4E`f0QTqft-Iy1h~XHz(9T^*ML# zXZWIsn-WJaTe-TnOn1+Sdj3bEI4Mw+nPA3^$d@`M0@{|S=y|@C6-=ZDN~jxJ&Tt~-)eC$ zYvT16QyPvphRj&Wgy3o0E-i%WPtm3@*?8JIQ^<(!M#**gVe|HePYe#s9&<%Gz#RZp?f~OzdAgHCve6^!l{Mf}Vi*-I5 z^kwa872teA>?{{r!OQyTepKwSmi{8I^qT+>EzqSL{4&HxJl5_XszIT5-n>U`*?fW}5 zL-e+GU+tpe&9V9ik3aa|&=V?|FCw-obj6lvTUNF{580Wb(w|!o&Cv7UY~*K&kMq|2 z&g<=*q|$O#p@q{np#T568}7d?PyF9>PpWHr*;B<0?r!CW z_M2bK{(m={xVkkxhZF=3%8TAL!gN zUfI1#<5gC@igv5*{K+;`e2gDG{@S*)_6(1hbj8+()1=Ncy;3O6x>XlA;mDUW`<8?&Y9193@%pwk-~O9LUtplb zuCiryJJQ1Tu&m$qZk=q+Q@iEww;yJlu-m70d6dx?ku@#0UNvu*oY=|fcI@ARzz$Qb zy7kfL&vR&4?Amr)?&G!MkFLpa0Umr4ZHlW+G?O{^cqQj9z4X#z&Aekd?77?K{(r+F z^ZT~$?+MBOFR$C%DfhxI{!Q(@P5hQ+e{8-LFP^L<-;n)aP3*+&(R+V0MP1EdtyU@7 z*1ebUp+@3+ty|C5yv|dYvtWZtV=()BS@i`PhMQJso2Y)c@{wElc-EE^llM#zbve<+ zGh5qil2Sg0)7pDJ!9Dy!Hj`B}jufTHpHBVGcWv69OC^S(CBeN{FV|asKERl(bx&Pb zMw7GZa_J|lkUQ7C9?gFm%i`~`=H7)v6U$N~C!{7Wjn!Iqq~g9_mujR~p@7$?kov=y zZ%po2U!-}~yf{xM$E|+%a8yD>Be!i$~E?CuP-7nT{u}N^8zL|H-NN(CQb$zyE&nS-0&$>f@^&f$tVa zpI*MtzV!OMBkR_j5Zt4AG~maDm#?oeX|o<|jZon_xJg}Z?e*_bR$ES;y`@PZ`y=)d}uc{$zf@VCBanvdjWFz(i2%{ek-;^EWJKI?d1HaWApchl*sGdwhY zg~SS6+UK@yq7cjBN7JADl+ia)+v}C`rs2NnbFUY_a$wLD2-4t><+o&*8kz>+|Ay{mE%Q!%=>vqdcRRq?)$>p42{hEILTXY zYV4|oS&B^tQqB4^O*=QP**-H&ul9|w+Q!E0&7LY7^x7_M>aFT4yq*%KIsKVwu2rzG zf{mS-Z7-*fh`=UIo&+xCuT6&)8!Vrkd9&_s%!My(`*mlOAKUR-m5=?!J(o($9Pj1b zYF~I$=JweZUyw^_sI-2(NZndI*Lv#qH{5&6ZcArK-WJcb-m|uNaWa3zx7gWb4Gx0W zP5U1eKeN!7b!P3-6>;w^c~WQS?zud>aOV-Oj%KCJ?vExnah#oe?V9psv#0id9(WpG z@q6#8W3OcJLaz8=O#Q}LFTJ%cJz3&=bwo?D^TS2 z-7f5CyQALj)&9IUd?z=B&$it5PhCgfp#1g($z^{}eCgnv@$q3qxL)mZXZ7_nC9>wm zZDZ)}Iks83=K({Wm(cN>1>FVGG3@WX4eza}R5~3a_%tkjv8;gZzArcAOE zHF$Nb)3?4eSLP(Xm=|r3{rTFao-^C_)_*(kcXIRbRW~P|c(5`oNlSrK#C@0Q|AzVu zCwX1gcD81rZVM-o_(?a76Q^9C_Fp1@g8bZlrFmb@$mlIfpY+&Wdy~V?C0l$hGucP+ zU0t-sM{rL>xXY5_mB&hUZ3wt|K zygXqRzm;G+r)8LxDa2A)(Y+T9x~ysGl_On!FdI?DwbwdEW9p2Rk+XA?Ep zBUzceA~z`KTfmZ;diLs_+oEHwOwR6FvT$kRzv`50s^aU`#_lk)n!@MzQeD(GG(Kng z@8+$Yll`xxGrvtr((IPls9HQ>+Kz11tD+yPB=(o@63h&1+SK59xof}w$K5yX_wZzO z$uqN_lbhex-{f}w&jF=)#g1FE9b1Gvmc)MQ{zYeK?Yo ztta^X{I|PCMeibHHGN{A?V0FN_9-J$EMt1gjE_n^(+>A?cxrY`)o$Y68QFK>^YPC2 z`;QjZ-?%qZF7|kscGZRK4PQHQZU2O&_r*AfZ}_?*ckjcaeN)|@RsJ+w-hRLI+*)}@ z`=i&m{qicu<74@?p^B3?YY{27F<33b=%g5cVEoj zvHN!U#_jn{ufN6Kx%A!m?!NBXwX^qH)QU^THOSmi3}9PqYrSk+aL&wNr7&al=AA0h z^R2RHcga3II6vaJ*|Xw;e-C08%ZY1WC}|R1`C;ed4=)bvI8ep>(kJ)e3LWud-<~hr zUY27gH}gtI!d;H%dvrtWjO3zyDg;zqnncyOlATx9EP3;6>X(|G+hXSING<%-_EhG~ zuRRAFOAIbg_hW9zdG_J0^w%KH$Q9KNvkhC9Z)?6b=bP20?cCC-sjHr@Ij#1swX@i` z)6RS?N1^cVpThUo$aA)6ZP>LwC{F9?>4(pJraoNMJN0Daf_pME);sQuS|=97%z7pM zerVqrX)ganrbjMyH4_$IT^d!Kzi&f`<~zR&B1t#BraZd4>5BRU{jdIRMOTB&jJHhm zUiD+jRtB~+J0DH)S+**?;odt(zg;4l=5b}!mlz}N=Qrs-NxnK!i0$?*+Z!EUrn61^ zzbC~>Hcv6!If$bzWYbxf;++8ztEN4B9R1+qmD5bg6Ls3A=Q2NWN#;;-xVkEPPVDt* z6*pGw+TPBr`FTRfiL zx6VIM@kGw%+@+$@kDecF4(?#Pz1UuO($vMpRrwy5m|}NwIdE*T{(X`E!R`VPy|{;Y zV&~O(^m31__^j2rK5AuEZRjnVt6v_pUUZCDzcu>9o6hMvKSI4GEn->{JI^{VZicZh0QG%+P6D$bs_?)BTMT*gCTn`_1L56Ve;NiLkDKXsyIH?O-)3%et?&%ZZw z7R|lVx4Bn$^V{;XCymr5?%L?c5;tRK<<^GC6G<)ZGmakk$}PN@Jz6lW?n}ne`SZ{C z=%_zPbd7&{>GSrt^}mfS%5|jKUhY>@=b$vVEz4CY%rO!L9{M&}7kLHDQ7F-cLx3TQ~$yr-nQ_n12Sv{BMeEeq) zJBK6}LzNDd_gQmh)*Qa1*lEsUXjGN;=daxPTq#{2&b+I|4Z<1|1^GL9LiwvVvF+2H ze=U8IUfUxNKaQ=3jxNiukpH8dFia_$*XtG>3Gt!`oX&^ z8mV7QR(!eRY++KcdT!QAyUlV77x-PtzPt7ME~B4Y8&`?cO+QifkarJ@qV~Fa;YI&S zR>=f^DOerJKWVG2Xp6Gxz4b?u?hEc+x5NLB%+W4a$HaRZ{*~IkP5!t%X~h(+?IF_K zHTJt$dSo|l@iFLOJ+@QRz_DiMU7lb6eV(`q#t9g3o#1*M|HI2ic~5hp$;TPW<57Yg zpKtz)Sxtj8PW#3siy3TlUEaHT6n*-0Rk4QWcgCMjQ%{CXH?-cB(|+nmZ>&Vs_7lGz zxC$?Se0V|P6didpDaTE_V(u-p*l9Jr%u4!WYeoTk!L4Qeg}?n6=Iq?wzoX&m{Mx=> z!dohj@0FB2e@=e?na&yJhY!!LcrAZubE&|tsY#Xd#9#UctvOIEwD;gE!$(qYHtyX& zyZJ=d26A7!8Sj8iTTkt~c3|QT|HQ>FkDL7Pis%!|-W^uK*l)S*pX`^k z3KXrTWq`6z<=#J(TA_*1RA)W4tZP|ShP{_vAR@^ppHxT+1P4dlZfNTFIHA?r8wMP zx3D_zzKrfRhQq67>=xf$lvcv>J?rMBf_szoS9HDOYuR&h=E;bxMN4-ozud5BdHdgW znTzdt7shc`^*SxF%qhr;*AeLdC^3DCQJQ$u!j%hiPWH|YRGw@lWunCFS-w~7T5gU1 zf%%7o9oUb~{b*Tj*|X|q^Q+3JWfo!IuCHQr{q8CpRaK=F7rC;c*Uh8*V1eZe&!>lb zFY27$k$kYF)zxIv<>J3z4>uq4I9F~mrStlsbwR2^57(UfllR4@WddW|!#TEgId`Xo zT2FrQ`or!U|GbXZz5ms}`~APn8}By0wSE7uR*pAm_w|jB%l|WM`Q3iK=}zkVhUk~a zo;{uDE9ob{qx#a(&6`&2KEr*N`**wF)&XMOqIPhE0$DQq@j zQ#;w{rLuZ<-N&=@^BGd+DI6+4be|zlfN_D`aR;CGuRU&CZ1F4KUi^Q{bBUIV-*S`; zBd;eW?iA~Jrp@7-@-=ElU6uda2Ng<|+qN6*nq?h$w)ec$;*U9^{Q-w>8nV|jB`w%d zIXzomWBcsc+G%%h1}#~$S}9uk$Nb{mHxC5mm1%CPexufEY+)FB_M%V5-_xB{FETbq zI$W+6S{a+X@}*+^x&z%p+y9w2Yc}Sw*4YQWw-@|)W${H8g@TrfhnGL_I|%zUi9WZx z)-&orcbbV}4$k;=bUSs%M_? zBBKc@sfx;{eCil}*;MTplk582^C(k$j{5!x?dj9R{DPaT>UVFqn*HVXquO~5-apsh zIFw-WIP+A^I*YRCw&Vz_t?NS#gycH@KVGs*<73hd4IkC8%NJub#WF(VxvxIH>7pUh z@hnTV`+5Jx&kt26u$3;KcW2IxbD2VY#&0g&lm1k4-g45jJ(EIKTB-Ye*nIW1_r#CM z++BA2taIe_LpQIi>2wTfc>TD}!YUx8s z-Q}}Co?3IgS82*26_=*E=(?R@H-mp{Jy4($ted*&?BNX=E!t6z+*8to_Z)d(b+gaA zgiZ26=r_AP-#E5KZEU&Mx_14x@1Gv5otw|h(!RqjT|1nzZ7E=P4cs``9)Emt$STb;G(#k1#|E9;|UQAiD^^?gj{U50Y_hXk5}Q*!Dml?IFif#vT=*}`E+@u%MWkO zb6RC+vRysOy5{qb`9Ak|-oBraRm-C*yhlc=``S0L!)te_KUpeabPp}GxzaDihnMMKizQqu6;qxulUQG)un!JE}f?!dP#NJ@}5&c2T!mnXN87FIo6w4 zMO{AoX4}8G;J`Is@}s<-A1t@#kNjrsTXW-Ibl+d$xt!VW&K&}9nuJ0Y`k!?<%%p60I`i_2nWi01JdcGUj%=DW zM<+n#=xPzC4yD*h*UuVtJo}}>q)?rA@O_h(#W$I>>sP%>LN6EmwbnUZ@V6(#x6{r| zMd0k}MYk_LTJ=oh&>ji#^8z}LrwbpQ8?jquQFQsblGo?7)>$)ell{ZGi#O-W+|Zcv zz7i|xY|TYyXZGJd*VS@+f9f{#G^@P->#mkP6U_TBcjLxQ(Sw=_`DuMKisf}b>Pc3K zPVceEb4Z(-IMZ~qugG&({(IX+&jj0?ohWG)ERZDrb)QChn9jL9J{1z_@%0x^7u0Y5 z=P#4QJg2+W>U7ngYvv!n{Ik=4c%DtsM}9_E?N{5aQHQ!S-cMSw;eA8lwZktga<1(^ zx?x_}Gxle8AK0J%Oqkyt@Oo`Qof`AX>#WLAjNB}R?|$-sX_rXlP2Q5kcsN^_&#Qmu z`x}Maxz`NN?9lkhzuik-B~nrG)RlG*qe$)#HS?bTj<$5w*9%d-f3Tgua^eyG9h|MH z5BgPS3TTAIELWVA#lHNZ^EJJx**w2KFp1BQSi$S2(xcTM_;>#Ej;v!YrV&-5CqH=x zE{yBFU=|`;DZT1CCzs-+N!fo*TBf_V9(Xv{=2q?2k6N~;{+~6km~{G++_%L%m%lE* z?VZtQ|2Y2eKc6a@`hB0ze`J}UIqA3A*_@BdobtY_^<~|3@pv+U@m@wf^Y^v(2gBB{ zRGY}gR?(bWd!B!z>8(UxvmZCFM_#*S&$L=Baczacf!DdO|0iTW=bT-+{r*w@%QtRq z+Qqf=S>kwlisR)rmDs9_W}jGAlwIq{Km7aPnxDEGPrrXOd#`lH zpZ}??3nV3Ti&rl`oL2nh)bIM4%^$Yz`yv(^*ZpDT9oGt7u|sQCZV+G-OZvN4WSJTV zTPpYG-#;!W?8=gtI`PM^osD(cO|8oXD|c$!Hivpm?pi7o#(r*wk!O>oklck+r;c|_ z{_s0NXiZ#f*t^=mTQ#R{K7A+it>F5dR!Mc~VwH2pZXUj@KhHjV-ova7Z&U6_3K%!xZeRnHPOZnb*$UFEdl%+#1o0=d&R z<>eO|M9wj_xtf%!5fW*8|HtCBALrg%neh3!^2Lrf^CV83xpsJ?^XbEjSAD&znP2$* z!4n^a2(F#K_U@S=b!wV;R^$KEcS6~=oz1?OeRJC68=jY!^<-aG$z^|9)@o>c`S_VL zJI&z0;>-}yVkWOs5F%g;5pmrWDuobuz# z!DBP>T5hv5=Pax%TIRWW;oV#Nrm42g3s@78+?CT27SFjmDBC(XaP5zm|Lnc)5Z_!`zge#M6hHU!4RQsPq2=ZGm?cvEvyzHm-f?XO)Wb%M%YC{*nLD+OIP2T<+8BPP>A+4aN8mNmZmt`iO(^3t~tf$)5-syYDUFzyTD@|zfe4aJ08LAk=!Vlhk z^m%v1hdnRj@WFuvU^HMn`}{${KQh>Ga)`_HhbVwx$Asif*Qqt zZAlit#!|$)Q2T~qOImnp_`=I&$M%Ti2~O7cW-B@M%O`K^k0%wkdM}GDTN1b4`uU_~ zQ;p6>Z_#j^o;O73JD8e>Q73%5b^h${O+>i$Ej}vR=C+3 zQ{J=i(6yqMi~oIo{o3B5_+{d%PghMXr@neus&aDDZMDxLC)@m0!*gX`ybBJT636Kf zJL|T=*6LIHPv_Xw*F4C2rTa~{L3idtS?|k#zv(tyT9wc)6`b4F7qWI??a5US&&|## zJz_ISC0%^EyvFx~XFqI^_mjyq)Au?$<)%TLPEmWmu%Y?Z{I5q!R#`6(EH_x*zikii z%ZXP!j{8UmYi_@&oXdMG<<*5#PlI{9p4#V}e)Fb_JvH;?_Fc1TUT!}aa=O4*B$MaH z>e6Q(Z)Cg|n=d~1;eB1jrX0Rc!A?^xtm-#@kGnS2^3Ka3w${z@i4za(m3bP=w7KQ< zhvkP)uRi|C=}m7_Y3bevmTTeCV}wp__VM zo^J@BCFr4 z=g95q5w)HdXy;m*JbQZinJ<=yHZFEP+LF9>q2`=8IsYx&CoGD)7)vom4O{4cz}&E!;S51aZf$-iN|Z&851MUV0= z$+#KJ!cR&YRzCanR6F>z`O;Ty;XLOi%zG4??E1T*mwEN>tVgfkm?fNFcz5&WUhWe@ z*Off3KKl9Uu-B&#eP5S&Tig)K&+Txz6#Tb2x-CkgszTQB{k#O*TMTyuxgKf=Pl~(k z(7I%HmigEI9R8k!@NiEK@h-pO2f>-^XBWONtSAfp`8@ua_w(mZ?=0I^@@-$=?IUc* zt)|@mW;<1!fn#s8Qrmy=&+8YR_}ce|{pa_9CszC!ethoylV$3-ZLdzAcT#v~HKXuR zzlhhpJne{zsr2Df!U|^RTE7O_ts_q^JP3O5}~1?U*6v&(p-FRmG+m%yTX1d zZ#*ShpgyPN`KfifPiD6T_na`}U0%YOCBFQe<8jY*l|4^pg@^9GrCE72?NIU2w%835 z=hcMlT{GKUaPkw+mn%8W20Y#XaKZ9#maZWT-_I!<4m)cn|w(D{)_kQ_trSojgxl_mWO4!$Y(d|EGxl~~8?oBxs zJr`QD@9flmcSdyEdix2rZ`-?@-gO(_KQ{Nghq+bYLATo`wuY`O66$5;Djlybv2A*L zT0id2w(GjVBC*O>I$a7lUi(g8z{%FM*lh9ts7h&>w$pPo0*-`Fe#*{1Z|l{aUw

v-ew+9X}MbDLu?I648|G&bN8LxamPw>gUOJkCL99^A~EmbI8&A-0f3(t2vtb zPo_vf1sHTF+eVVZt^4bD#Xw zY=x^Qmu5_#>#Z*|*H^gpd6nq0>-v@FpK6`hcIwf$iI~`q?6q@_)m%7H!JM&&YPuF;PWqvigy|3l8Q_34SD6+s3|CsB z;4b<(BlGE*|3CWo%=+TC?%7rs)ya#Oyfl~c-nr>PZ`kb5<Q1>Nm|ka_jPk!sT3VYbU%{oqG9UZjEe-wDqy6 zs~_lntoi63ATYmXf{5dGqmMP5!Nzwu#Y2{5yl9V6IO;sVLMA}qqjSwijW*`1-b*y@ zD7YC1GU&*Y$b0ilcweV6&~!m`R>sV zXE@kX6q82UwGAnYHho-*BBa{9q{#CQr9t!Eb!Wv-6 z`nvSR(hZD!T!FQkB}{of6;GcStuOMfPfNei$L_V8y`|xk_HVbg37M9kwD0Th%MMrn z6x86fH)P?e3HI76&gF~f*Q+~ypSUt|x6clycN31V)v*a4nf6Al&PwpavYom6PF<5T zJ?<|4$#+wk^8Ehi;l67&R=w(3mCont`cwbtcHapi|D(=HtWe-AxV8THlXGh)ELb*2 zfUSMI%#Vc%Ib6@9Y|is*2WjW173=+Dn|-5r#+CKnI z-P?RB)h}xbuL@XJ{kvesxzID>>po7GxxIhcPQA@Vk(2gq-mS8^tmVjB(O^ygJ3-$9 zWi;x4MigxB@th#vM6|4C(*B zuU3gZ_+M80q+8K^x$lQWV!}L^=$5-hPnc3{B*WEet$eYwF{3+i<*K}GQD4uVuG)Pz zz{DY7FSF#rT^Sy{tas73c)T_g7SuL-VrB=wPS+5-|Wm5a9a_UHVpmJ56V?j?z%i_n(8`Id2Pt!QMv3){u;Nyx5uQ{_gwU@Lx ziUM@^KMHu!Ix*tu|6fiPO#9pBJMtb#Y3#h$mi>{-XW!9}T+W7iN4S1k=3ThYbu}sA z!L|R_%cd6QJ&w3jwN2G~lZ?9GoDz*Ql3r}qA-B%?Ot0KNcd3z#v9Yo7{qMN zUZHm@dRg|Ks`~HQlDF#aT{GL4KP~=q9{Vc1%y9ojtu@aq7!y^WPdk<#TNhTo;$-d@ zhwTr}$b0PW6Wq9|aMl^!u-o64#VlM?e(o@r%yluRDIawIJ+ZHTdA9NF_q!tO@`*Bj z%*m{$U7PC`Z|S~lvz6V*bpA!fQ|wy3FD~Rv@#Q_%_06?G*7PQ)6eG_R`$~?huS}nD zgh`+7>yt}yy6|-;v*o-KJiOL^zn7kwSQB^or|d7YJHBs^r7Q_}ImiAN9BrhJbNT^qA0$R#5! z*)Tr(#`?v-?j?O)bZV7tX-eYvjbF2Jzb?D>YVG9fGIQqc-(*;O_Qe~=rDsJmjpw%| zpGxytdUjo#SL0Uo+NCb4TuTkV@TdhG%;vkg?tCFzME{32p1aHek!;a9)2@cQdkKh_?jInw!tI&qs=_q;i}OX2YqlY2P< zipSg9WUuNrUp8}lzLQlZ=J%oJOuq8({&0Oetecf{C5!EjP?Kthj7h{=6^^WTVj+F0 zmkLi`i~kf1kc8Q#|TDxZ=r$~Bl>z7kozg|>kSGzXb`4`8cZ{cle*&;L5B4?~V z!>g;ca;A;s%spjQ&t7;{m2JIbcS<=UPDN>DXG-bH4G|gdUX~onV3IKOom`^tPxNX?JmB(A(xz9SL*&^o^wU5ydqb7j)%D$%V}TE{CWH-=5wchlIeB$CdYjE=2Ewv z4xApP1v@L!)xyKuh3kH|pKd$LA6c;lB(<9XYob?}x{*UkT7dH$vUcOE@8^|#j5y+^MsdF)fQ z(d6;&4%;~rCw3&SUV1D_chb?d$?X0%FE9PN`rT?v*&0&|8=H!=9^V+sSZybq6PeZ> zK0niU{@yJ+g1D62rE5j~zEsSbI!m@WcVbC#VAvlG$5l>FG6C9$Wp@||t@eJ*-d!wx zdFRt9-KAMBO8C-YqqEXC&NsD#nNaKoG(6MX`50u|yOMyx*6xWmODe}h#2#2HI-9tJ!Q zzcu}0_R|$l>uuKUOIEG5EYsbek#fl;LXTA^scrfMG0oP=Gia1dBe*YhS|*u2ibP4*>&<0&+#Lh zx@ImrVE9@s^616AGF8i6gO_WbzC7F4_SB>+2Gig9$hP0BJZX|K$u)Fm`qCA0Q@pi3 zOY@xdFX-|vcG?jBFIr`<$c3W^CLQ`U=hGI$3AeYt?karh+Nakav@iCPt?=(vg6f7F zeqUH=daAJ@`|7uIJxl+runye3LaU-M@9(+umR0+&-zhz|u=3S{R}YuZxBmX+u&Jf4 zr`HyN*o710?6$8tz4%H&{}ib(Nr%aXE?+uYX2^Q83AAW(nC;uYXvYlel3z!KS~}%U zOw{5sD1KdD_3q;Dy}$B|U)}neCwbfG!`pkd*19XNG*_*bJ+$M;`LZJuz8rpRW_IzZ z#krc>N8Vmf;E(lxv?MiBS4dgc>lXi{CnAc$lX8rjbS|fVDDpC#UaNQKP+^}!@N&)c$GD`mW35N8dBA=v$S%&x>!%tM9*l?zEyubym8U*4I?WX}b>V+i$P^ ze49V_CvWxTz}oriYMov$Xx%?uwtiiGRpN?$^9?8bDQWs_a8-PZ=?>$l_I$QUSN~mI z*3f^uZgbJ^W%32*>vu&iJF;tyaqqX)mRf6`?6;7v`($rl`S-y7J@+@>tFw-Jy~oC3 z_Jj)Sd7|^~F4?%~WSQ0c-PM6-LrR|J*l)~`3aZ`2a`OMT)XK|eW*$1>9rQN&)4$yC zrMOs6-_!ZIt+zO$Xd7F(`|DqYXD7}v-FDA0V9(#r^Oq@V+!fxHn_qiv$N$}i z2hVA}nt%4;zJ**l)wi1uc6_NX)XdS|YRoMCV#BgYu7_gN)~KT| zZYs>@y*E$s$Ar}9Y>anuc81QIlXI&%kN5T&?>hlSHBp{!#dR!ZIrAhWHJ7(#FO3Q{ zmuN^=?-yz~xI%jC)c<>=-Yu*3UY`}S(ECH&r7wJnkNO&RcZyp)YIWM0T_65C?0IGV z{@CBU|CYHw(|v!`kw*!>wx1Z@Mz|T*tz+qGibo zi=H!cS>ANld*5)cu_oTjK-#EedE}I1+3q5 zNB8};#j{gu6(W_dz2AQAb@`6J8YWkNHt6KDJ=HFnY9%z~`L-$3cR#)R>zh2&9%&W% zNDWqx2)X{1GHiVdHEZK;CGXAe4~j~BcI*05EBSf6_n%BE?U$YM?m?1}@Sa&xa#>H< zy=J~_HGR0t$z^eY&ix2=7Du7qWN(MsY_;!LX6--K zX067&Cm&g*Ce4v^yZpgScTvu&H4&3!N=>WW(Im8zt$kUT9nOe`rFv4lO;Obb~6aNr%#b;dv~nX zwE3m!_Z$mG-;cK>ry5rjZCLcjInI9Xt?XE*ZS=5+B~bsO^5e(C zYy#H0(~rve&!2Sb#^voN!oTSlEz_}C@joy?KI(^dbDjB#PyF9>R*OW4ao(y~VE;6A?Z&#-`d_2_+4nS-iN92pv|Y)WJAG69(kJt#6;^id+-GZR zW?*GvWOt#X`lO4=v`-JuM3jXmFPJC4g1_&5*UpbAXTDB#o1VxV%X(}!&K`_dvn6e1$Iu|InjAq>zd<6?T7VSTFrMHH8{L|f`S;|f$vL2 z-mT+hvzdN~al<1^6`2aBWUaP?%QCYX1y81IUgAFC3{#E0{rqhf+b2!+3aVZ9{PV4N zMw{tzs~AB0rWqKRe9qSC9Gnr)Z}XLbnfV21+xCMcA6UTjIvdCT%l|2{MX;DKFfp(( zFo8rtXH+C7q$DK7{hQz0*m(Be|4;R&Zz#=(H;74fVCs=#SbmYcouT5^)Je=6VjTrs zs?(EiZ<|~5|Hii5*+*|cPJq!odaGpbo89+Ut=(svzU+I{H;2lCX3gs=6beimM81yjN^eix5O)6gzaIE|E$PT@6!3gGr;M)@^zk* zMeIMH>VKW`bzy)7_askKm+4Y#F1MxZl~7&$bJ3C|kBaYi%?P#I9QC}p{oB$t);Ik$ zR{l1g-BdH@&ZIvEIn%oD{1%a0C1Gj3ld0=;$PaJPYn3am&Ux>W_iLb=Ir_j&b5ozytX-`-_(9Plr=4RHb39=U-fq%6i@AW{NXRlp=F_G;57i0x zJf6KOPu@TKr{3P)#JN7lW}S9=xmRP2_=l?XE3RHM4|x9Y;9k$UcUY`9%ccB}u-+_} zYglT*W!L%tifFOrFZGHeCs_GkCw{-avHoUE-^8|Z%ob{Y~)Jms`);WisV61RIE64&*cqn0Z|2 z#;L1pb1#a7u2{Bac0pv(TC12)jqk5m>zBCCs%PKR3_9V2L5CrhA(Nq$VHd+yhM$b0 zjJAv(jKPd?jG2tvn0T3znGQ0uFuO4qFfU+U!@Ps}F!MR)Tg=axKQjMe;b0MAQDD(w zv0!mw31Ep~$zUmAX<+GL)nt9b`hoQiTQJ)Rb`ADC_Dc2^_6h8B*l%)3bEt6`aU^ie zxOub(GtPdozzHPZCcaPZdua&m^9!yoS7;yeD|C z@IK&u!~26Tgzp%?6n_SPF@GI@C;t@w1^jFHxAPz3KgWNI|0(|m0dIlz0{;Zv1p5TP z3Iz(q3S|nF2-OR{6V4IdB>YyyPGpYA5|K5c;-Y<`@5Nli>cviqeHIrJ4-lU&{#-&! z!c4+R!cQVfVy?s+NmAe3<-R1#5+Kiq48>m5P;hl^c{_ zDt}QaQe{(}re>k`Q~k6?q9(8AMXfSz104>XPr7Y-Mta}%FB|w7JTmMx3N(6dJk=z| z)Y^2fnStK25L ztGGY+==aRP>klxL))6 zgK7VTQjOO|xKz6R+wexM&_!7xyTy;UU2AG%)rEyx&HR;yQha+}l_-DGR@bS2;k)|h z{Qv9v--|cYD7rGa9Fg@9UGJy7-uYu_+Li3-ueaApC$*RCc_mrJ zxKS(09~5>uM~(_IIWQbJ5Pj$F-MeeKq?XJ7b6`wSlU7o!xwk+5q0ql4Ka<7clolmD z5}Fir$!Js2C$289Q(CLCUP;Z0x@EPi>K9v^+cC9eY0t!_gp~9dKRHK#M+_mlZ zw%prNZ-veC?(Y2l;Ns+T{kXlA-ydF{o^N0G_oujkh>)0|sPOa&QzlKDICXM(KuAzn zU}*653s){(yLk1oxq*q1nW3ri_6=J$ZQHnYb9q5YQQ1x*7JfGY1_p+Pb$^#v-QJcf zt*W4*qNAjxrl+W>s;jK6zJ9@qC2JO~TDETC%B5=;uU?*?kdcy;l$Dm3n3&*XQvb5iTJA>|5*l*#imA!gEu|%2zwq+(_W1go zJ)^6$c1>>1+c&Z@bLYg$txKKKo~NuR;u^aB`#(`ZSvOgUAFZMBJAJ0S>s`Gie*Z?x z$0l13KRi^eFF4O4mhbk%r_Y|nSg0<%zj<1~y~paSHydOA2mH|I;bmG z(7O6roSaRh)UHo_a$fRJt7goXfBNTHO^$ffPCo}$&F9=tZQmq3sC%-fnX&3qi~Ed3 zCHcK3ZjINcv0wl7lcjpb&ilpwj?AW&2W5FKOb)j(yUuie#}RJ7`6{zMe_uA?gz3&E zwyaMVL6Xrnhj@f{9`;Tat5E&^MmuAH8vhQEp?v)Yhh5|fe|=|KzJKBy?F@y#$)BGH zy-8G?{$Am{c;ld7y zYn}IAieQO5sr#EtYQ;%eJ0pi@{uAqxZT;mE+d9+lo7$ffKf!JJw^{jljcCOYQImk; z`-S$0cwWw*z5Lc;u9x{NUxm)Ze_)qC^F7D(@q%{7G6N^>yT@0b{5t>S)q-Eun|96e znb!XIjgrrXd=c5W4;|Ot7E3q({Kn5aqooQ zO~*ygec1Y_GvsLIfd0KNwSFf;L`yHpv zCaLRuLMTF*S@*)n0D(_=>S zisQ=-4)U;032@})-EzT^pLt20Xn+%|smTQ=c3YkaRZW(uO;hXkh{rzIA%Fjj`0JA; z8)el_|1GkL(Jh<&^!DzjIXfcxPoMmBy=wc8%YW4${ki`r&35ObKc5?J-@lfAI`M}4 z=cDi1&gvgOdp4eH&z-yV7A>C_U9`$rP}|L&{;IjOZi62GwU>3dqS@MWM89WW{yf#k z;IN!n#7swrpK|Zs&pWYwu_5p4e>>8Qu9e0Z^Jd$Jg$LBnIAiuYaodi0^UkQPet5So On!9|$|HZ2=>;M2t;o9>6 diff --git a/webui/public/assets/fonts/rubik-arabic-400-italic.woff2 b/webui/public/assets/fonts/rubik-arabic-400-italic.woff2 deleted file mode 100644 index b42c25f4f6a4363ae9740c955a3b897f2f708699..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17368 zcmXT-cQayOWME)maK6DH0HQw_FfcfmLBv3OR4mnKBR$Vdro)OY!9F;|F~o(TwLp-A ztLdLi(?$pGRw)KXGbR=0&I}d>)|Mdl*$F)C(n`lnb7s5o?v`!)@Z9darI2EdOrw0x zH8zrm2Zfi$a~AE>PUK`S9L_g+U>Qrnx@y`7oo4#1a0ohy-~CyVRo~_y51I?q2=s&UM@U zznvX~wG=`a_Smf6;i6cfWSSLn$c)cp5>rijMcwSZ-xuZ{aa#T&(Xgtci{q5d(G^D= zmUT>AU3N8WeeBk~WrA;a&fO|B@4r=r?e}-@`ZH|~aI)wKb1n&bU2*bM=sJ(@yk|Cl z6Wnp?qUaF|aYw2qlW_VhEX5)->H9wN-npm1JtA79gdI^@p zA#qoKmaTesRl-vHyxfChv-S($|1DZmP!wR)+%S96r*$7L=eaMNrB_~9F0`OS*uG)@ z&xyaLWHl@aUi0SE-dL0JV#XcTGRz7cHVuMH{&_kwhICmpv@<&ja`|Z*Y6KeDt6Q~P zcp9)gqNHqTva6L-S=!U`(4`SiH(mr|~Ik7hF{-uAbQxwby>{jX{Nmh8RilVbJqS8So{5}u?b@l{7(zxw)r#;uEw z-rmgflz4qwU{0r3O}U-`$5w{r%y0M(+*-}HLH>Xghht8m>Xg}cRHwD4d}3`9bGPYK zIVd8SpTwkd`r-i&ji{}Rd!8w@1beDgoMrM#(_nP9J|WK%!;s^nw<%2_JhL^8qeY;) z;Bu#Ke1P5ov#PMeb87P(SRT}@2)g{>mrl55O#IBuRgU|EuX;JOvN>dXqJk$5#fs3lV zsQ;un&a2aTKkmqr6ZPrf?qXlJ%1!@7SHIcH^rI&vE;(+^smNS-F}ZXtUuf>tDVvhK z%vK!LZdZEhU-u+g?wOn4GG*)F*{LV}e3~AK)*UMGas0){{i{D)CP}cl;xOYfg9dh) zgtZs76taF8vaa{f6})4_ctgs)X-W%+pSs+c4;P)yzc_A)xn$#~&#`4|>BUKx6$B2S za?oS?d)>M+`e>4`+w|6sH?N*g+;g_r;?^^-Pmj`T4trm9J3pWIA)jy8?af->*L#V0 zWPMw;sKM>tN!NaN=d!~a=XOauzqrY8MO}S*@NE-c#hrDz9o`3QK>Zn}sxwk1|M&RdakdT-M0NiI6(&wrU7D~&TP{qZOE z-m6dh zsgDe|{X5%pg-yWx1Yhlg>!%+nGxr|(XqFf)w!VMaF;2&@Da#`ByH4)&iY{JbC#By} zRrsV}4MVa6$6lpmsZW)wpKGtako|CL#3m2^$ZblstAgi->3?E4%c`J%C}MTs_Q|D} z+Tx|(9ub)Q$MmX>Q-4l@k-hzqj_Akb69qTf2q*VAK3Fh+L#0?*Zoq;Q-`4S|p7nci zbiq%%=T$!w?KgR*e%Y)dqQ-XV%De4k7XdVU?F$<^El1+iz{(Z+SO-U1qL3=seZQz16eWzGlk=?K@Q$ zZ2aY?+^Jf1e_48nu9eoNc{~hUnKSbig!Q-nGWP42{rtSx=#pvR#LwTa&6`qQwxlUccEKenn})qATmyuX*FOx~0n~)Z=I3 z!thS{?o&%EB2H-RKK>{^s5|J%CD+FnIbQiM+s2n}Ghbi*UZh-_$#tWKPf335YxkB# zt>RnKY^psQ=)zs(?I<>=Pfv(0TkHe}59u_$5-}A%8$GOXlVP*z* zvEf*=df62E0`gd*ccAH0bi$0fpx}^NBaJID0yM23h99hj!*V5IV9a5C+ zc(*i!|e-hvAmgQ-+V15M3eJ(^~TPvMv4Cf z?MzdYgk_|~|0&iptkSsrG=Fp12UoAA>>sRh^*O4W)>-PDUuSf?n(2d);H7y?r=vO; zBBpJ3^*d*szIzj|MtM0;L2lpA{-nHZe6pH`($liS%H}6x5UvKiBvQzA)3ev6ZE-UgHYDjhK5b$(HQ?=M*RrCh$qBQrw?F$ngZX}- zZbO4mv`vHG6!sU{Km0RHKNYGS`rYx{{{F-Md;KdnzSif~S^B6WQL|)cq4X-2*6qs4 zQmrgW%M7KMw}f1?72dQ`G^w-GdB&>@gV|9A?lT^?M}D~aTcOo=Z|K5Lnm@I+$~~SQ z);;BZoRMWkw`kds9NBv+C!9`XUD!~1;McVyQND3CYva|lziyN2Rtr70dGYpuqju9n zmRRW@uh1wtwZCAVSM8d+fuT*mKJ@1?@q0J6rZPRWOAYJQ@Y;0cXl$v!nvu$7<(w7u zcX!_X`?IN`l|eATSK{T~fF{vv-kUFOd%CLhm1skQ>fFU!Bt*oQto?Z1W1-zEzQmm+ z`3$;IhDt`FIy1iO6i@R|`qQDJwKAOh#pZJz3nuMjW7qgA_P}*NJ9`m#`leNm`b%H4 zC9Gw-y!zmx8`1e^^%)*b{C$|4@xiCaxe5>diZxATc)VQ6o$pOh@C5|{CZQF&%x!FnBetJmBWrKm3c7g5|hw06iIG2{+@PB>DH~X@uaOT;zP~qhrlaDYiYJZ`0 zOQmCHw+@@K_lXH&jNSijG8T8+2_=h6%ek>I`JrHeqKeY9gWWz20)j~@9upO#_px?O zS+(UtkoMZG_tc_uHXqjeZIX3)>2b#8U##9ODb{;ooN%HeJ8wtomDumLUv31S-}~iO z^!wT`H!bXEJW$`?+GxSh+0fV^X7wWRK*vnWH8+cn9C>PPIz_&90l zb; zEMH`MEzOWyU{A=uDGdT9=D;cZW}t3#WQh{_NvV zJ8P5V9JcH`vE)OQ>y^`!{>e0K3z9y_@MlJHl}!Ebo{!!_dv<(y^6qHcf)1Zl6?cuy_*@4wZ?xjB>fZ;m_w^ z2kz@vgmHY;@P`2 zKP{YWWEa2j>MDks(~}xJ%9}s!@;uqFAYO&zi1X}nTMa9L8(!Ue4J2;vSonHY$R)ol z$L2|=PRyPibV-Zv)b?%dvy`nLDl$eTI|!Op1wb6+Nl#}-)@XB@h8Qpf-Mg+B|L%Q$icqm)b>*Iky|>3nK; z8lUWzat-k<6&bHCWcJ@x{rNX7V2$Y;7K@UNZS(yz)}NjKOS+*uuHmJN!?T^53bnI3 z!?KiH7EbO-5@TMozKiS3#EJ#nleOgBDt8vhDhV$Q6%2o+K42r9fFo_7X?-;# z@p&g#Ti;17T_4YOdHUst98uF&X1RL4MPjM2PIOn57l%TH+VrCt;`zN!fkk&xuLL?T z+syUqY0%l?Hxdjwfu;xha(6Eh@fKXY>*$~Rg|`2cWyGG}&V9;%sLS=d@!qiimjjg& zPfWE;{j_P}^}mxZimhe8&Jz7YXod3GsC8*YoW~C^UsG{9AzCl#+9#m@YjgI_4l|{@ z7sXmse$V~8tn5W?sNfCG6I}(3{4OtQdd`2~%45~ zY-m_{!{AVz&cVJO+vEd@(%WhoB5CsF;9Im-`8hN{^*ua`XP zJhJo0#~){Huk+1&bY!ys)!27mHo6C`d6p;fUzXjq)l4{8^peE%Bo;>&rAJSs9FN8- zD=Nq}22}2mWm_H*^E7G)bF)LrCccx*ZdE3(2H!q&6bg#0c9>z|_;b}ptDd{rQmTJX zo#RxgzSQmVYh%v7#WPr3@~%nfu6&Z+bEskZWy9kUsfX?eH@gZ?a%5px;?bdVb5GxX z`JC5%yB}!VZ(RS>Pd8ofTDgQPbI|pv=0`)mF|JpeTJA5X^Xd-c!62Pw?TY>jc_Xb9 z=e{+3CCA>BEdOS4Q#zAFhRD04DZC5RA}hIqU6*`ta$YL?@XU!tk(TFM6hA)f>P4fjLcG&KzCl{l%3`9y@MIaC5ye@x8bBP1x4H99_xtEo^7o)-PLY+aeLQ z_rtf{rw*Q^A{D%8kx^8B!r&&n#TULdnl`PVKK|&ke&dpM@x z@zL6>AmJ*>Gu5Z3m)Y5$e^az8{kNNp^ynKAj)>0f4<-x}B0#w|Kze935wc}Cr? zyy%+bo9`>P&F=S^8j_i!{OH@H?NtspyB2}fm+U>yUi;#Ej^*j^6!iqjg|aoB zTMk=$225G8;F9gKy^PMEFFod6wqUaO;-4|+uXnmmU2OGxRc(Rsys$qL*MEMrCn@vS zk5B5s>n?tn^*cev_?mr2f_!9&vev}aibtO9K3_W{eD9gZI|V-+cpZ^>x1&2djK7^# z;$rzMgZneqOsPM*?U~Jk{|vSLYxN%goF&Z0@_W0?W$`sHn8O&Am1-HTY5)J0q1EB? zrKB?Rbls-?uPn3P-#V23ht>3TYC4~Nm#&V}^etDGisY30S@#MX^0oicO!7P1B)xpe z!eWnO`=U;qj5NvLxbMHRwS`6H(a^2?ub*N4t^GCR-T|++M30Kiue44aJaz40=i{|I zzw~n!&y9%>WKlX0Sl}kTI5en{xdYC5RYqE)x4+FCktciP1tQ;p5oQ}@Zvo9X|ls%(Zs$8Pg52LHE*{yzN0 z?737+I%>y$HZRV^&cJo@ob8V_Tba}}n@`R2ib_1EQ(vNBH+KW~KJVVwHL8;*P4W1m z82WGL?Y4XK^Q$C!G_I}V2)9>xKJTVWy6%zHT(0{SyVJg}@VK^CIqUN;h2z<4wi!4` zO762>w==~5bj7pQTY2xVY9IaeZoTr(cfUB4lJu5dp1(aQb=SKOXZOuAo8eKur}nA& zgjw&6!otk@Whhs(@R~y@3Y08TDZqM{ChA+{G8l=}PTO>BMdVhl} zziC`_^D+Pbz8hHH$pn5-soZH=Bk-egR{fHVUrK9aT3>T3dn$(}O_?OtX}GmTU{Ue5 zX&+n;&0^lL@L$JP8;jEkl69=Iw*+hD++X`UTHcf?@^0m|vuJyA z+xMU9W!rNBsmoe#Hh#}NHNA1A>1^$t??b8?zJHH*OgHz}XvH~sO_%0+yX^aKqP+UG`qi^{#8zx|*G^;3x>J8mW=(C!1^elu z)l8f_k39NceVxTURlHf=TgX0^pfDg6t} z)mxRyBApi>&Hil`RPCU;^wQ3k-|jvM;PhVf_Grl3imiI0k?++sfAblg-*sXC;)9M~ zgXf>`Jmh&>baGSV@judk`L^kEJo+t@=l9;XY3-v|vwuhS>Pk&`VVCfFr|JF6rDh&C z-(6cDuzyKaQE^X7??lxzXI;OZl2%{9U21Zcz3TkjQxm?v*Hf#U^6u`pD3jlH2Nze* z%j$+9ECYx6q?7v$QwUYVNG~P{eO5V@nTSP8sR9~5=!{Tgp`_hZbtd(DS6*b)V zmt9~oFDc(}bMHm1Ce5ZNvn?dec3imFy2b9&++|PQ^GyYAYUNE=aSs*^P~M*PWBa64 zx80W+T-lMJ&1hKH;(1Rl>w=A?I`?eLS8|n&mXjhl)iOOeU$>T(|>Se!vZJ+wV^!nBpj%S}vk9z#>=?*C$O|hnfJ8Y)4XsWNi`0czwe^yij zr`HTg-L%t8&HmY~lV)~B+Oz3$zx%Lk$IA!BwbLFd_2r#QRV1(QHG!t*+Vj=sFqj7`!wx@NDB)J?1TEx|d}#5E?d?bH*O!qrb7E}fs4^7d3s?V4>S zS6xGA{FD~#36yR0?GCt`WpXrUyUlMAoye&Us;r^Rb**;8aFob-+<{=56_O=TNvW{Ev^d$(BC@A2aR?Ivb% zws+pDf$~$Gc@|ceuV2IKx%qUpaHM^=h~u^^vLUzTc<)L&azc7uaL0Th?pt#6Zuj5f zN>?>2yE@xQcVcdg@1$v_0qVzFSN9h@Z_L-zTO#ZBRe!f@1LM!M=8A}YCF>YW&Yiz1 z9X0#=PWEc&*I#bi3B>paD#!_?RxJ5P`E>4H;>&hHUA$=o_k*h#lxmZ(WffB|<^j^c6e zYPG$Q#~*xK?&BwEvGrNcgo*Oo&)2fconTwXe`osQs5?hr-t|_BJNLOIT=vo?kMnPY zi;EI{uIL*SUYK#_$C=rsnNHk^$2xxpyl5*E-JNhZd2Z}6&r91?zTUhU%kEQtB`vd4 z{BL2uZ}#RAq1%UUaxuA2+bDE)pJmdCL%Evs{?@Th_;%_Pm(J})4b>A?uq)_A3Gatyxz1oDU}$-0{eh!<=T}(tq+V%Qw)(o{6F!bFdY+GO z&u}l@zIn>>FYi8F&2X0P-gWqMu-LuCq=`Z)CA(RsEtWq&^ZA;Z^|@)4pL3Rd(`SFG zv*wc(zd&!pWENMKEY_z<@e9pUtxFX1YM7^0Pd7EKo1A%7(_!7qr?s^c7hkez`z zTVm29^|ba;ug2kdG4GUT-rFJTvzue(p}zF$6}AR{mA57&uD8dQrMI#?>fE&X;5qH>^W8FLXxwZ{lA|HOT?cPBxLAG*z0^;)S!80ZNQ39g(ttyY<#ugc%Q~&hOP5I z-DaH69ddZtOSW=ewM6!x>%Lz9TAcl9+aI+reVeY>>#s@L`(#!AA4RF!Rex`;p0xi- zGV8;Ht1+3T0lc%9&;FFPxAl|P^?Sytv7Fzhg)GdUqWZB=duDjlp~ovL_IO2q3cp=a zs<(Ex(|n)G_0wWGD!$GLo5*3tWxKX!-lXICU-LF^p8r2KyZ3atd$N&((+sylrTxk! z?`)>zelhYci7sRJu;0yf_(6v2CK(A?c^P&6!=Yyj1npAYekayTujn^?Zar%c>y8Nm z3uhGsMentCP7p|0B6?Bo@f^crokq*D!XGi|2z;=3*SdX+lzw@*{z2o8393A-CG-D% zHxP4Fk7VbMGMuqt!53FeA#2&K4@&0r9%OkPaMXk0x|`%T;UA|9CRFT{4oK;kzm8S! z$TA=K8WxL8rF!>5=a}H78?v<~?V7I7lE2wlXLHEX&>o*P-LtJUe`{uJ`|#s;iSRkS zzv|9vp|aC6lkO>9`90~ORFQ|G}-RZ&&^p=#xp^$*-;?fzyyJF)nP zSM4;-!>hKxpKQh?;FBr8@b!!Gms&QHw6hQA&iUaU_Fld7ZoFXrlJ618eX5U@OQ>V3wdq&;c8|km^ZRL!+_9OCQz3=5nllsZ84bB<*rFqx6cCTdlVAtn+kZU=A z?=r`E;j`lARQ>Rb{dlqT%#>ZTzCD@nr{$hd;R)^YIVbM!tp50p@l?;{lP|4#RveDu zi)EX7AyDIu$gMYvMN9I7t2A7vI^DhaM5wwg=(^~JJ;Bck14=V@nq7}l+GzLbByZPj zP0i1_z7Lyzhg;x z|DBl?zP8xz!eYB<))L7OPHX-?Su>IC#}4K$V6$Ngv-Gl&OzE(hx@g_<84=mM9l65A z5|*d@D_7c8J4gQekmJz(aHsw6(mUVo+n;b_a;KEY=Bdu^im~T> zkL*0#b}J{bKzaphe}iEAZSR{igC$(;t*?qDscY_&InZW!{7IOxW@*6f^()l2Ke~}P z{oqT5Gy7*A@{=$P%@T}NoPA-Izv?{RGpR{ed1W@6#N_B~5$n#CE*D|A=N{30Oj;!T z@Zk=-dbK#qw-vQfAAjH0In&kEar5>?lU-JupZz+fVe|3Lf(cKp>mTr!Ufy9@Y+K=Y zWX(#0sgHQIOCJ9zJ*-z!cX-OxpU%o#de5CMU8}eITlV(77X{a`AKKow>(CZ;_uA$@ z*X6y({^*FWtBQ`PK5ikDu>$Hbm``dzi(gTe%I&ha+yUwVcU8t9iPotiP#?yzj^V)MW6K-J`OwmYO>x|r*%>J zYncBV1*(Vnf3CFp%etHU|K0ra zPa@p6{JbzH{*`rKmE*;NP5XOYxuOk9--xf~|M1^G;?KT&9lL+@xF+*%5=!AqSP<*K zQL$dZ{NC$x23dR7y?W@@euiDYGI@DMzT>OjK3&Pm`?FeRxG#Ca%loOSod=cD~!T77qJHI@#R$es}5{w(Dg`#S%f8>a}B zgv|`d<<$_an|w7SY}s}3FtN#x_+g6_#{yOMWXdG-->{QRmk-zwTIHwLin9zeK{;GRh&}+IjIlj)Jc@-0ZZpZN|lOuQ%>w_D#Jiwf{rz zx{b^891|0Yrp~z^BD|;a?RvFiALe`P&@fzB8Dnrkr@XylaS(@+s;~RP9G;e4TSNa( z?Y^^AEVI62b-}8yF3&upD)T&kO!9h>!r0sOy!w@3)`gD4SHzem1;-^XP4v0`Z0hIP z!UxU$>!Y$yR%jI6IrqY&_Cl}&S6jWMRY1U61J=NsJ+bq2wk}`eVa3MbR_?rCOX>8K z(7O8RR#j7UT3X(oE;@H=frXD=y3PEl({4OBednEAl*M!LQ0wkL%iUr-MB_ZX zPk%VM&hygI&r=sYxd#WNZv?#UGhcba(N(_0mf&cbY!; z`P~+=P5q<33QM%WwP_N+xOOg$`K&l!|DBaB*TH?9`Lgy~=dDbhXE9yo#cto_Sy$hb ze!XjJva?y_`SY+Vt3O9w390)xNojec;thlMH#p62PdN7Z&90~0JX%#hFA;9<54}{l zH($W&LB^b#)9>_NC~ob^W_qXjeE#yec3jWa%74DK>$3vy`Wv&8jaPr&-}>J8+Ix1# z&A0Y0&R-{gYRSb<-#nvuHp%%<-B@SuV!WDl+l(rEfj5RGVP}@aY@e^D{P)(b=>EN{ z_RcgaX5J?tv-sn>hRy90F8w`!=(}I-o_#m|KRM{ry=T*%m>Ksbypgoky1<^tueqyv zBcCnfg7T!fj?b6;OMXxRIlu>36Tpw`Z%z+*G^b3ZOxSRnVg>$EyBO)r2XCgyt?-9)&_BZ z?Q;xX7BzMPMZ9ww7e{2rEH`fT+0}7)YWP*%m0sVK8r@gj_xWM3!9KMpT?LpU3 z?*;$Aa)IaeI%{oc09^~i0*s}K4`Lo+p;c%B4fHwTx~Le%Lnm@0UBjg!9!N)x!VVa>IYs9GP$SjBB;# zywxS@7uICiEs-mFdRxcu=tcKd{&>bqp(*|gms{&3hUCco|8wQ=eCAKT9Ag~RvKLn# zp8Cx~U_;_G-ebF1&+pT#IqHdq>c`?k~hz8&HAQr49{f}G3u$CYSYmCt;)jd4Dk+N;K(8P0u2mm1A< zPZOBt%(|;!=CN6k+Sm4)2#Y?H68yd|M44@0+rK3Fr6qZtG2T;JxI~lU3*SX2n^b*} zo;AtLVe+#_V!2W7vz#a7-q$jocWTXQv7a?bA2knr?ABEK#wIdXaC*k9&8IDY`g-`* zuU>mz((`Jz*UiU~tHV7HmfdjrKP`CIy({akWbbv%JRELtyzh>qQ*VA=(tgcr(eHWKb@WWC@g$wUdw%+n9JLGMD~4uxSTU9VEcVu*P~OU+n)SUOf!}X zoV9@SGYjv$u!;5Wg1$9PcYkMfdQDnwX2kc)&u+&*U%@DwyF046PT0OMUNhyxu{JOJ zvtklke_VE{<9{&sN^Sqb#rsch2rCtlNaN4u!xE*^fr3vw^kHQkMd8M z%W%3`@uIiZ!s)CgUUJ{AOun1R>ngzECzYaH^&+-y_s-2cn%p8Gr+Xb|=bXu|UoG@x z%EHOoPiyxrJabd}`1yOndeTvO{kv3xe=WTqK9~3F>6)j)-bPVDdFLc6nC0wKkD1>$ zTyyTz61xfVS?A*vo;u9m^Uhb#T{H*Lr+!uK`P>)6qZk=W8Chy(_CF!bV&_MP%!TidW82C7Yi|>2(<^&X-mc{h7Y; zxI~|_Mc-i+uR|NfcCJ03q9m(PXUY_JC!|Z)+vR+?@YPqFk3N%J?QXMoR_Klm%DP9r zZv}Oy#U5bylxh9wD7ifNs*<{o*aVfoyLuebt1dfn3D3H+wf>T&%fIciE&eZ`zV1~ubyFAGTw7aL$@l5eUK7s0We+AFzV0BNT_bZdqMyY$Qc}f?`wrVH zm2cL&I@-5rMpU>LZVXsy=r}44jj02u3VR2se zPS>5)6ZlF_MD3aW%I~(Ho8{IRp;8^0_`uJV!DqLdcB^;A{nnLse(vj^EIenw@s3H$ z5_Z&Vzxhq^nw!E78{gZ>=h;nyncn{n@BUbVx<_lAF?# zyM1r?Bj-4q%guutLuNg9X64hx2yljO_)pdLCS zJ}CP-*X$de^RJ%x<7EG5*D|A(-jAbl92N35WY|x9Qn*u1?t9Pb>*tIj1=g3n&P!iC zKd>*cz~qutaP5nQCj!0K+>PS2u&~Z8PyaM!<2NUbv)&)Ni;o!V3!j;!G(D;_In3zx z1CDO)q(4Tv|I&q?q&y7e2;BJmRg%{6#mD0&N`E->@TSk%4c7I!dsi6FWJ>fdX5%_C zDYt*I#VMKVa}zl(DjjEAtkRW`(o?72uebJcN$}aZ4$F*Ug73RKFYIkh;`lt-vb)t- zYVqb9yxY#UJzFQ5Wm@yjGJEyMlqGBHtl}Ta_}|32sh^bYomu+dT5!+%ZHv}!og@Bld43{C=JCWseo7XON>3tK|FL8r zmZ*^SQ|Fx2$o$$vL+J3^5~hB|vP&N9o_;FH$w6<9-p;-JYn!BSQQO(9z3GN;t?HPz z=iN-Q-QrsHenQgv-bP^yrKtM)oV(X0SnE`!4ogP73FFwODf}6c= zCjBv#RGtu5c(5RZ7oJ||DY5C`!|JoQn~ZXDHTd0n-#C@ul-QTDCOfZW!W5yB zn6eiWblHyDbYDEK(x{|iXJFt|R+)KZvSR^@I)CgTetE_UUzy*(SN%J@wOVYq#`lN+ z|9QWZ6?2M_b0(~zYQC#4(7$`)<1r7qV-poO3yJ4iTm5i zlr|cut=yR1>X}`1;$66?i29a@$18m0yS{Y4a|`n+TW~Q{>E^8^JNi;5ZSXFueEKZs zJbLRqDV`?$uq>PZ=oN<+uKL zxi*Vu(lft6*K5a{t$w#zX}vjoy-Ixc>|*gdSM<2q|IRUu)v*@;=NN5sJ8n+X%aXJv z?UKt`52Ix{V*2cl74{gO&v7~tR@vy`VANOk`*-}U{%3*bIfWld$*8M*`KNEE;g>Jn zzxe{c{Di2T8=vYk&qzoVyLZ;#r8v*-}S#c=gRDvws%MQ zd)1I*svGPKlKTp7WaS&Xy1qWVJ4^Cd)S?S2U8heu&I_1X^-Of(?%lg?<~}=K^Wgr` z00CZ#hdGo7hFtmc-8f>+cM79EHd$c@#)Po znx>TRyHy>OZ1nT+vTZyeQ_pStG+$HhU9kUUgP1iN+pg^7e!X{j*)-M*HK}X)J~~cY zrt^ozKHYll{|hVjrpKEZ7Ra;z;(vU%qjYWVtOc`LlOnlgPTGA-xx4G;wx0{$+*`V5 zpSg9TJhyt^9rZpM<@WHu`|dy0Sp3#9N+zgxPI$)0 zH|$+{d|zEvGOqmiDp+_t>HhaU-`nq2vv$p4+j7C#!dhJK!c@by#%XPbf=uRr>i=fd z+Vkrh|LOZrSIkKD`)nt0;OdWyEehv9uLztLwBT!0tOb*O+@apXf;N7KigN4g^uGyyTvOIRJuKcCO~|2lO_YrdR(TtQQ;`DEtTd51%8-7Ig< zioNpn7o+1@zi->7ni$=`7WsUF&#}&flNYy7&s%+@ZA0b#3qc-?o;M5T87@0xyvjoU ze~_GTM{X`(1mlYfVGC>QzIH7;b~O5s*s<#=XY_dt+s!0u)d~BX(>P&a z$u7iX_ell*yDyb}zi??{_S@ZiRBY#bs$J^aa4(fN$n(RY`5V(+ zB4^CooSaa|In{aomS^7Gh#3SF@e-74H~Nv$t5dclm)UU&@c42*2|FQbVH24P}YMQ#Ur=y7!gcu-Nr2=hLLQ zVm4lrFS0qTEu0z=o&KTucE-&u%3CIQHxZVTE^k@cH1+=%*@F`H|5i+k&bj7s+bqJbJyUvxMd@joYK6Zu z!d7UDt-b5;^P{iT{IUXZ19xrv#LZVyo@vgmJ=hVoSns09ucxKXjQPvkqKfyKtSbG@ zp&Bav?S+VxOsFfbpFQIm*JkAjZ^X0xwwcX4_WG`7N7nu~wtY*zTq921lmA*`>b2jQ zbAx49ySsDut1owZ?%9|BS-SV?m#=dYzO%-@E0TDi_VnQCPkJvt*%a@4w`;-8 zpV!yf|9tZ?5 z@u{{|ce%V;_y6tiwp|#XS9^BT|K&$#-<@su{B=*@yFcCud*tFbzjd5u@N{C_RQr6D ze6P6s^X`6rIK5=!o%LKCJ9P}(*=rgm{(ExaP5x)y_Znxz+qd5H3N0@Txcg?KmeGOd z=N-$Y{GH3EWDc^`igl)TFT5vze5!Gos?Z)I{rWMdcL4o zWkBSMo~u%CEh=yQ71*3Gqc!B2Nf zMz8o*u{ypp`+s5e=a*BSo!Y+4F7m{^3ARr&=dC`vdqOPhn|*O#lMA;bMLaq0ugt#t zf9%B2($;*l%3C)Ik4~`A{#H9_>L0svK6Un-FBt0sZ1!0u+ltRm?SH22vd{8i&g_1X z{gEnc&kcJn*v`24ek1!^v4v^h)Hm}S{i$ZR{)1G7yv~FL;=f9#DLjZX{LwILTKwyI z8f)$3Kb+0o_J&PHo^P+@6Q4C_{iVHY8Pc5O=Y>R0w-0<)Wh`QMQY!vIm}zy=U2kS4 zy{mB)x2e~W9Ef>vN1A);`*me& z*IRb0o+`Vl@o3T#OU>60znqwqlO27;^W4uJ8k^KCJ!j9|{Ba(``L5sgY*(8jBE@RD zk2R!kpZtc!T4=-L?`BbJ(-*f73(0aZqQI2ol9~X;sE&t{mGtq$i&8|=1 zCUf}CJu3CxzD8#M-~Ny-)3TPDyZa?oouAH~SfS4}S+GK$$C|%eEcD$RPiE$##mE0> zyq_}J$g5I2F^2h+nz*`V&WfzF_C3ds`#y0}ottCl{`m6mCdoMlbL^&nI{HpWi`|ZQ zjo0Z-ubih;{CIvLB`cFnxb526U1ihee4eFatR(jT)n@Yxoc}%AO*crtIQF$cY2M)^ z!+CKjOE%x%Ih)yN!bR&>lYf;zmpl=&XU^PDGu3nSxho|8O`Q6C?XkHt=a=4<>{$8i zRzR5UyS)dsk9$a-{Ql*oO3bsn!uP`qcU&*nb@I!}{@dH78XMot5f?gm&HCn|^{F$X z^+bzq-Sp`>`YzEp*}-DoC4R%rk(ca$rYsg;*YrZI{fmOYS>xV&B}|3!9k=p1dQRu8 zxckX^@eC_fQJuP_b%%{PB7TSEpIz}he$mql%Rb4*cc@NLxwn1eM_XfGXFevTh!g&j zYqnc+TO=NvT?eDEGzgQexl!mMuELdsp~va&q6{$uCbXnZdI8?8M;j zni^tv3-njK;dxn@qNVx!j=jy4ZgRA+<*M! z`m)(I8TXc+o3yU=@6xj&iXH4N+3CxC3_U#;o}Rs`s$qqit@@{llP(44`u-5P`8)l2 z%XqCe|DfSwO?xW0@2Mey~ZVP-l#oo z{lz@6R2J#Vgl&2)KB6q^oZ}m-bs+ z`R{sMN!Hd`uCaGcZQj4bXy1f|iR&xM-c?#`_!Yj3p?3Yv@5|1_ZCM?}Qd?&{xk4r_ z%+53A-UG?+5{AzvR8*b(Gn+&A?OpBfQ?AEHFN^qjQAuf+S9wjo^qU( z%2WPy%X3}H4wkc4GHq$he7%BKr&#;$*DH&?pVOrC^>z?D*PNJMz8v;{H^R0~e`xb% zVRp`nzf+5D*j6gBB!{cZWXKmYo{fC3pkQo$ruNPg!)l)$B3I%aw9C1En%!L!-!OB& z#^tR+E_s$|2@@~*M!z{(t942HyWw?F#XXv#)nDf} zet!FQwOdYJq5p?d(>Wr)6oy*(Z_55OL$p#e@}Bth=I@$L+tYI;te*UQ^X!JR>>gg* zHra){9L_hiu3?_<&$8Rb8xL~LnZc4{AGpPQp7xykF)L+eie7en{#bhJ z5y_A zoGm|&^(bWjmVL9`oHM)Y$j(;zCH->8S2RzW<*&4&XY*$PjsxQLKP_MWImWjBMY+wY zy#@{M|Me_>dFEeNf$#6FGn2abnZAAF{dI6{=jvI0*$d3dnC^O4Y~q!DG+80O-7G}TcAIrf++f=Cnp2Q}g80NWn?rQu{)XOitGK>+U5|3cb5)LN z!yj=;o6b+U5`3*Ymq*37A>r|(yXISYJvYwX#Z>SAXiw0uY!{Dn{y%sRDk?_23TnFj zQ}N6>r>d_nUfImOz*fY3z+#2S=N(s+Zz`=?qnYP-_q}6JUOdm7=^Tr`z6#iuESjAB z((Ay<)S9rTuZwfn7x2m7`1a$TMXB(kUzR@8Z$*wQ^-3-q22OBsY2UEY)Mml6S8KiR#eDr4uzFVn>u<9b$MsA7ILuygR$I*$ zoGfA2`m<+E_J_AJO0!fiTPtR=ZgQ6k*Hr2?;rCzBa$=T1W1Nq3w`$$b)Q`+N)MSFQ zUR~y7#gwr0}v%!=Y)vIE5|JS&uwh(7+Ju zBQLYIz@sIoW`k(TVq)8#C=evucGT^}!d)Bz zeT<9|i)TytwRJTzPCua%?xQ%>Ys=)KB9+-y2|*v3Tw89e@qFp4nXb5U^ARE4_Crfp zMLq9DS-VOVmV23bwa=&HnY$<{F9Vf zE(kZswLR|)7XN(q24h>ZgNV!OT`JnEmPs#5wbm0twdC-Yoa0~rDD*s1Is0kR znpex-{z{zlEcNcU$0hq>^1mPd{PkZ=eLYbc_Q2KuUthuzI7u}0GVkS0ve%WBRE%1^ z78}pdF>62ln7hR>;is_ok`+9`0U<$QfuX_IFI>5F?c&wT<_0E4W`?H1L6)~F-gtKI zKlHce^Y^o@_NUnmoU)^fm+xI)_%3ma_WrvE--hn}`uJ#!#{RO_;65jL?)^a@H|@)M zpZ~q`Ratbz{2xCPUdJB&&bHvu?Q5|cu19*l$$b#3r#U6$CFhBn9kDxw{7##vH)&}| zpWTrfcd*^jQPxW6T+I3}Dtk;sgO{~R?Qm_Kq+`pfmffWkeCFn%uDNTkRI5%|%;$K# zFs^Z~+4P>+#_+eFlfRd}>Xd%>tdg?o@C#0vCHXkgT6x;ZS$$$_KhN146ut1)x)VE6-!tl+3+Z*deR!4ex7BB+ z7N6MS%)Ilp+24OBH(l?O*Ad^Pvz_(t{<}Zs)K`9c^m+Dn|M)*q|EoR*?>g0#a;~k{ zOnj#EkNCWD=X>YhJbn>jz-_HVRWO&)KaK;`N4%s*;BEZF{U-OQz|T;&3iOm{g{ zcQ1JL;^M7yN_QI~SB7$Kx}dOxP3&2Er`clHDNl3KFHIDD*)+jcYO-AGnR^^A896=~ z*VxSFl+;V*Okxs9beef|b2@{PkH+STZ;YlhW=5^e$rI=b($U)=SATzc=Yj6|$M49_ z*de#?(EZI^YG$^7{=UC$_4vB&lm}ZhE$y-jnQR=HXcWi1e|L3Dctl$1 syW@NBrgb?bT@$fQ-1l$MpU=O~Iv?ZcmsKv0eDnCs+^7%x+w{t80XGwu^8f$< diff --git a/webui/public/assets/fonts/rubik-arabic-400-normal.woff b/webui/public/assets/fonts/rubik-arabic-400-normal.woff deleted file mode 100644 index 3b48a3e5e27267ed852f51c2ab32546e4339275c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22364 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 diff --git a/webui/public/assets/fonts/rubik-arabic-400-normal.woff2 b/webui/public/assets/fonts/rubik-arabic-400-normal.woff2 deleted file mode 100644 index 673f79567582cdb3a78db65c9e131362ed67a750..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17348 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 diff --git a/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff2 b/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff2 deleted file mode 100644 index d6d59de0acc9b9f51446946bae6092e406e2651d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8480 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 diff --git a/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff b/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff deleted file mode 100644 index 0506d064a84ec8cf7e43051f76672356383ad6e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9928 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 a_dR7|Wg~B-Lve20sU?_Xf7~;XuWFW}F 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 diff --git a/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff b/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff deleted file mode 100644 index 42098f8a829104abfbdbd8e10c32f75509c895a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9004 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 diff --git a/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff2 b/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff2 deleted file mode 100644 index 109a7ca52b427983f9ac59e0ba756701bfba55df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6880 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 diff --git a/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff b/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff deleted file mode 100644 index d47ad615c2bf54987af88f2915f29e4bd95172bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8208 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 nTnZb%P7>}`lst0d#uRv}E`;oo5*U8DU#M diff --git a/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff2 b/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff2 deleted file mode 100644 index cca969e329a3285d94fe4fe50b5d2ebcc11a80bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6252 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%0DsFvC5u-$_-1^4 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 FMgTWJvOWL+ diff --git a/webui/public/assets/fonts/rubik-hebrew-400-italic.woff b/webui/public/assets/fonts/rubik-hebrew-400-italic.woff deleted file mode 100644 index cb34551ba17ec3e066729f0b67e55cf12ea5c640..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6404 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 diff --git a/webui/public/assets/fonts/rubik-hebrew-400-normal.woff b/webui/public/assets/fonts/rubik-hebrew-400-normal.woff deleted file mode 100644 index df2536016a067321ec7f38f4b78da02db7541b2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6352 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 LjDg|vol{2ulQ-dW diff --git a/webui/public/assets/fonts/rubik-hebrew-400-normal.woff2 b/webui/public/assets/fonts/rubik-hebrew-400-normal.woff2 deleted file mode 100644 index 658d719417130223d0940a76d62ae1da79df2087..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4876 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 DtQ#uL diff --git a/webui/public/assets/fonts/rubik-latin-400-italic.woff b/webui/public/assets/fonts/rubik-latin-400-italic.woff deleted file mode 100644 index 458d6cd9f9fc41ee3d4055672956e11e5ae5a835..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24212 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 Zg)L{+#Xf&HXZ!xmm*4$iRxYV+1po{=JQe@| diff --git a/webui/public/assets/fonts/rubik-latin-400-italic.woff2 b/webui/public/assets/fonts/rubik-latin-400-italic.woff2 deleted file mode 100644 index af50274a4057ced6fc245afbb61dff4027ae69ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19196 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 JwX=_?0RZ{uz~uk{ diff --git a/webui/public/assets/fonts/rubik-latin-400-normal.woff b/webui/public/assets/fonts/rubik-latin-400-normal.woff deleted file mode 100644 index 25a12df4709097ca119f20d46156b8c83ec77064..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24000 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 E04Q@}$N&HU diff --git a/webui/public/assets/fonts/rubik-latin-400-normal.woff2 b/webui/public/assets/fonts/rubik-latin-400-normal.woff2 deleted file mode 100644 index ac6ff6829dae9da06678d1a471f198ad247ea171..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18856 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 F0{~n&kIDc5 diff --git a/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff2 b/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff2 deleted file mode 100644 index 0d4e484ff69f39b51c4b7d278249621e32abbe27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11304 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 S&FTwoUuS8!*RYv^fdK&ct!rBV diff --git a/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff2 b/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff2 deleted file mode 100644 index f8a84d78346818590d94467bca2c5fb3f9e17e9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10584 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 diff --git a/webui/public/assets/missing-logo.svg b/webui/public/assets/missing-logo.svg deleted file mode 100644 index 1006b8346..000000000 --- a/webui/public/assets/missing-logo.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - diff --git a/webui/public/assets/placeholder.png b/webui/public/assets/placeholder.png deleted file mode 100644 index d157b8f4e6deac53a843b1ff20506c41a7bea495..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6049 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4mJh`hQoG=rx_R+Sc;uILpV4%IBGajIv5zl zBRpLkLn`LHjjf)L5?c9xZt?uu--dT>cUxQk7I?CdyNSEI|3g58;+|5Wl#E5QiTM4e6*z2HY_^Oq;YD~>84E&PMu~I3u6(DDEroGd}ed}_tJAG z<<(Wc!?&8={ywMp_v_z(-<9uszsIhL@A$cNzPU;&MWzn(_dQw@ zT765`Kg8z8p^K$+K5Mfcd>pZHeeR2obe6dTURR9<`a zKWBSP;*F1g9(F69=x3B*uyD|O6Y}X&8Mgq-$!+InhS@O)Fx9+mtabmlZLL#lLex)t z$H<=y4y;GmZ)pY#Sj}SC)T_?O6CV32LnvhL&yA~2uX8#1t?Z;oTc6qi)%UT+UORS( z9GD$Cos)6jw#Ppg&(E74^L4}D*2VAJ6}~5aJIl_%*7RDh`uVE#TdLs_R=3z!Bs|~T zex2J(j%~x5SDP3Vug7n!yIQ}+dmY!d{RLnCPU&W6II>#LSRkhQ=&!5gTfEn4LrAoxBQ|!|u7#q$hI+|<9zbH`OAg6bK!?VgmM^!&qwh26%cC?6R z`PBWg-Z4K92yI~y-EvweMAJU7w9->cKH?AygV=nQyUS`4I)xcB=CNd-yDQ`^@#^TD z?qbb_>YPeOE(ljcba9Jr9|AyF96d8qJ?;_XfLJHP+Sw_zw*|EW<=kwL(x>3rR{ z&&o`XUNda^SGGw-@$PkY*RVaTOd65b&fC>LzW;wRfe2c6nT;; z@FjfR+nN`N8;g(YEjX8dRYde6g8`;Tqun^QpPL?1~zO4Z5>y9?!nNuI7!_Q|4GV zhJ>-T>9^<>-b_ba2@1)6-Un7tND9&p;%Ab+QN{tL6G%(2&4Lmrio}Zn^ZF2 z|9iZDPr*6!7Y-$|jDGW`ge~;j6W^%g`S~}KqyEhMyjI+wVx(j%o+!<|IdjUR8^-fQ z7S8eLniv$KyHX?Q_MFr5??qpiy%FX;ZJf#H^~UnPz24{V@2dXX>f6Mm_EdRSn8hs1 z-|oU^xAiVrVJ^4Fuh##w-?{gdsmg&1eP^Q?YglK(Qcl-dUctHx4vC`^3E;c6UU_Qo-#fEx<(_j?&kWM$cYnAq*R}J z&vatdt}LT}zs|2-e^wwNs1#t%DRIXQWpe^V|QV*8DKv470(tEbYL7}~exc6KiL zDb0|*T3fod)?IuRgyj5|0wh_aJC8f3>HRlI$Zmihz zPgabz{2jbbDuzx+e0OXmyRpWFi-Jxbl}kR=MLub z;^dgdTK&zB@;tCbdbqnvZB?t@N-sa)#`B{T8EF|10NK@kEl|B2< zl*(94V|{ijL613o&6<*f&uiZwWoMkqp)u7_m2=Y~hp^BSt9Cs(p=j~^*pCHj@?Fu- ze^2FSh|&pLAFE<;EJ2iOgBPp8^t0X<&NVTlGtIf#zgs`7Pn55w&q=s6 z#V~M+LFgodRL>b&CpB(}u+CnT;jMA2|R}zn8v-9M;2Ywq}cdbj}Npo~mn%uZ%1-DYZwu5TwT%Cr= zfBL7G8K$0+VGmAToRIgI-8gW{$2)?|?CzyJy_ptfOBW=ZE%qv74A^km=!rnASf|h$ zxid)%{1X%c4SoyE%`s^2tiG3!gTKit3B+*I{Q!DHVPnQ_1sQ z(uT>oFzppLL+eRTgX6ABliik=a<&L~wUy=f#V&*8MxMqV2ui zUZ(Xt=WCz(E_YrevO1jKVcOnn&eLOaY(6sFv#nsck}h~9f@fup*j%?J%bm`Bxr*jy z&jR-e9N7E&SK;Ho`7g7cGBZ4uIjp=`!0y-Vbv?4{=XB-2zulmD+d4UGy4MMRlf7|^ zpJvQrn6vTI?D*7gxziuK?q7GLf8P0Rmwvw2n6a$)rG`kI0~>8FpWN~Gr0DUbpBjsP zSli7z{ORe;T~Ea(7aad*{P832d4;7>#<`y!3%`iowo?^Nl z|MbD4%hLSET>ptFvUCLp&&%Flq1)+RciX(2b2r1!8pDDu#iy=)XrAuZF0lFB{N!lk z#T|8TtQB-SJ~b9yulb#PP)*^JdE`lfSzm?zLoqp71G_u$8useE<2;+rhLUwzHcaIojVweBX_P7js>S@w0d+PzPM z-u%1!G56Q%#M^u^)35e$hb&Ie{&DpLS58D_&a+vOI@bgk81$mwKic&5^A`qXr6BQy z>0R^A`|q9C@KR^(%kyD}P1atpIxiFb^k>fOrPKUoxjL$FC;YCvBh3D6zu$t}3D$lY zbCq|B9P^Ql+$-bBdG4T$^rR^3M3ZUi8=hyf{NibhI1v+~!N|h!?A)w+z6Vz)-rL7u z|EA-XmCaPQu=?a@n_nw8oaXrV(U0}K{G9#uOHUm58g6x?!Y%ox(_4$;*z=Qi-}hf- z`#GMyH_iF4#iY;Ycy{$|zgx`yqDe!ZK}LMX-{%Lvb6s7M5XZ>hTmJ4(nf1d>8Qyo= zxbIE&l=$4I_(lC^Q(NO{r?f@RyBXdkf1BNUukJuqLeJFSrv;uq&3XU%@txl_{!#)z z&P+9{`=-t5nqB7;Fc$S4g}t4irwy|FK;#!o}U-eTX1Ks&+tR`^y~% z#NS`pP@nN{;xrx3nrB|(;`1&04KD0&RKBn5;QZt7Qa25YdmdgPTW^2gwMF3+`>ABE z`}d8{8*I4cnK);`6=~)S4!Ma8mZ!ZNUZwBR*|}kJ24>Eu&&!(AeWfX`}2Ln*FST6suW{wpZtw&cyAeB zqjvacdYOa7iJkRVv(t4m-#2)g;D0YG)tXatpMq$S>k$yk#MBLxfQ*D?M*}lY7XL$FG@g zS&GSg^7>udAm(+6w_}Be+8+H$_1!m<7VXqq@SKHBd68VDZ_WLMEDilD{q%WwTtZBX z_OoU@&Ph;U z-F|rL=7t9s?kamsndf$>im^B`NP+RsWT8nVdvgBGlV0Ta=tuu#ML`jBow?ojcT9KK zrZMgK;{`1YEE=owodav7i@teF%-ACvVzPa%#tqIdwbQCuI9x)0N6hwD-SM=gL@KUK zpG}-;+hGHvuoaw^-mfwx{QhR9L~3&szTVrUc`3-|PNAUSq}DA=UVG0^Y!P@cp{DhC zV2(@EqB5(lTap)3)}9awd10QJrO(CV62k4!`QDW)F;7qLa^quV$Ia_AHUxg>Ty|j|D~Hn+=EbJ+j7>%}y!X6PSjC|d z@S~!nQP7DY>M_eV4gnUai%c565e!#td1m^DUCs4rGC$6J;mma9)7&kQ4DySsSvc4_ zXY}2>&&sk@xk-8Q{g=BV=CjN(RPjFVCR`?U(5|7M!EmX$tVGKSJBKsn4@x3-FWD(x zd(?KH{^v;zLQSQY$~0Ege+y7%>0=4Yl~)c%e~spZ_75t~K$%qp>*`7O7Djn;+L!5zTeD9-)_Sj-Nn*Cv!T_yk5xs`^ z&8r_ycouZUwa7m8e&@A(c?0PyHgyYEJnUK$lHhyFn_=lW%Y#=1T9Rz)cD`Q}{!>)I z)5*_XebxcdnTtY#O!LJye4l&iPRrqs47N;+*S_w5c~V>BWYG%YjMLcHdtsrzsZSI&&QACI@;86>k&~yZ_AxN1Jlc4mh$-Rn%Kcw8+SBo)PJS$a?XX6^0RetNefXXc7OGyWQd`P8l7Dc@kZq_1~rP(coN zpQ*J{#nqX~94cA^1)fz^dJi-vEnWU>k9CFL%g2__XUIsd^PK!?_Wk1@&c^d3s9gW+sT#D> zV%pd1Z81~0Vm734h|euppw?PrG`Uygm#*503w>v0_PtQ-VQ}~<@pI)wL4$ZN$@V~- z4@pwr-=9BFw7b}N;krx-nV7iC-{((~Jo;39e^Y9k`B^!R2`{{z`>QV>Enxzg;mR=2 zLhau5@XOb)GpEZ0FIFyiy~O8qq|iKz2}kCIPpYU5S%0}I(#{>{v11wrtKl(5+_x9Iug4Cn0}t*jA%Xfv-o%y z7hm5h3yWnBdo@`fyGmMmA4>6byEo}pm7AYPhLL-GPhw?P(pxX)nKN3aezai|wp?St zwSafq8P82kA!@ zzgCs=F^grlQu-;48zQdVixRvwtVCG7oenN5kZ61wutP?}$|!~{B9uK$lqpOzo2%Vs ztwJZmF0BPYe6C_0nleJV|9vxf3a1_pkmQ+qIKXm}m9tm+g(uIqDh7!bC%Avi}`!x?`+-g$B>Y5{MmEHifNNyd}I-NS#r1SY?Gn*y4+?%ws)I~ zmgyWj`18wu>x$KaiVO>dG^RSJT73?mlo}A(xJJX<>!ql{ab4B(KjRnM2&jBe75KAZ zS5xR2 zO=#vDrS5Cas~=jOOL;XXe`({YoeYOwsxv-Wtvze0lFVlFr?-6toH%Y9Z4a|MFOcwd z+3#=8526;ls)=Cq>FsaYx%aAlEFUyVM3*70Dcf_} zgjezV6#hBzzip1x$~(R9`!@dY_|>oX@x8rKBFV8=n8`SiVWG&$9QzPm74b}dS$4&i z4P8s?PtL2pE&ex^_wjV+-RrDE4T^*t&R(6Gd{QOpc*R0vb1&8VGr2xYc;}RLJHFFeUWaK8iZmd-L=D&-WcmmG>W8 zZ?|Qsf8knfhC)F`KT!uMk<8GGCa(RGlay8qO-&K-;$(FBx#s828Jv!bWajT>xwt7^ zX~*H62CR?s3l7R9$^75&Z5WDZhq{&e&>UtVuAnUvwd&&eO{d%r4-QqU{U`a z1D1qbxC;S4GeNQ6D?$!olYF~HA~cbzB^ix`+^hG zn|)uWaXP%oUB)Z;XkpmN%bE`yHt+v9o&R3#=ZK1}I~OjMlVQ9mx^*4Hj#Xav*-LHa zGBDYwG@Rh(Vd?zG^So)APy_d;$!e?YcfQ^oFPS&fK(^^_`JFFnjFnnN%zeM3HT<$U zKfH)F|0{Grf6*0H1@rAe)BKne|4i!$t+)Hj6Ww-={i9C~tAS9^>)X5E?^$`XBk|;; ziPm}d*dAP)khOpM${V+`BBT5o?v~&G!!^T#|G?sZMvOg;9p~T1P5-qz?DWOV8+-M> zio0~^b1sNj{=;r&*JrtZ|IV)88rA12d;ac^htk)YyFN3{5@1??(x>jl!`=T&jrOE| zf7I0fFV-#To9dKF+l%yyoW!c-e&0KLHuuA9_J8t=Zn<3LzOl)Wfq{X+)78&qol`;+ E0M2XxQ2+n{ diff --git a/webui/public/assets/schedule.svg b/webui/public/assets/schedule.svg deleted file mode 100644 index 87bff106d..000000000 --- a/webui/public/assets/schedule.svg +++ /dev/null @@ -1,204 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 0 8,20 * * * * - secminhour - - yearday of weekmonthday of month - - - - - - - - - diff --git a/webui/public/assets/tuliprox-logo.svg b/webui/public/assets/tuliprox-logo.svg deleted file mode 100644 index 7f5c089eb..000000000 --- a/webui/public/assets/tuliprox-logo.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - diff --git a/webui/public/config.json b/webui/public/config.json deleted file mode 100644 index 138d5b0b7..000000000 --- a/webui/public/config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "tabTitle": "tuliprox", - "appLogo": "/assets/tuliprox-logo.svg", - "api": { - "apiUrl": "/api/v1/", - "authUrl": "/auth/" - }, - "discord": "https://discord.gg/gkzCmWw9Tf", - "documentation": "https://github.com/euzu/tuliprox/blob/master/README.md", - "wsUrl" : "/ws", - "protocolVersion": 1 -} \ No newline at end of file diff --git a/webui/scss/app/components/playlist/_playlist_mappings.scss b/webui/scss/app/components/playlist/_playlist_mappings.scss deleted file mode 100644 index 60fc2ca16..000000000 --- a/webui/scss/app/components/playlist/_playlist_mappings.scss +++ /dev/null @@ -1,42 +0,0 @@ -.tp__playlist-mappings { - - //li { - // display: flex; - // flex-flow: row nowrap; - // gap: var(--gap-small); - // align-items: center; - // background: transparent; - // white-space: nowrap; - // padding: var(--padding-micro) var(--padding-mini); - // border-radius: var(--border-radius); - // - // &:hover { - // cursor: pointer; - // background-color: var(--menu-item-hover-background-color); - // color: var(--menu-item-hover-color) - // } - //} - - &__mapping { - display: flex; - flex-flow: column; - gap: var(--gap-default); - - &-section { - display: flex; - flex-flow: row; - align-items: center; - gap: var(--gap-small); - - } - } - - label { - font-weight: bold; - color: var(--modest-text-color); - - &:after { - content: ':' - } - } -} \ No newline at end of file From 4765cf6d0b374a6f28510c99c0be5be345c6a573 Mon Sep 17 00:00:00 2001 From: euzu Date: Fri, 8 Aug 2025 11:35:42 +0200 Subject: [PATCH 5/6] Added accordion panel Added mapper counter view --- backend/src/api/endpoints/xtream_api.rs | 2 +- frontend/public/assets/i18n/en.json | 4 +- frontend/scss/_theme.scss | 24 ++ frontend/scss/app/_component.scss | 2 + frontend/scss/app/components/_accordion.scss | 7 + .../scss/app/components/_collapse_panel.scss | 4 +- frontend/scss/app/components/_filter.scss | 27 +- .../scss/app/components/_mapper_counter.scss | 20 ++ .../scss/app/components/_mapper_script.scss | 80 ++--- .../playlist/_playlist_mappings.scss | 15 +- .../components/playlist/_source_selector.scss | 6 + frontend/src/app/components/accordion.rs | 31 ++ .../src/app/components/accordion_panel.rs | 48 +++ frontend/src/app/components/collapse_panel.rs | 12 - frontend/src/app/components/mod.rs | 7 +- .../components/playlist/mapper_conter_view.rs | 281 ++++++++++++++++++ .../playlist/mapper_counter_view.rs | 43 +++ .../components/playlist/mapper_script_view.rs | 69 ++++- .../src/app/components/playlist/mappings.rs | 38 +-- frontend/src/app/components/playlist/mod.rs | 2 + 20 files changed, 603 insertions(+), 119 deletions(-) create mode 100644 frontend/scss/app/components/_accordion.scss create mode 100644 frontend/scss/app/components/_mapper_counter.scss create mode 100644 frontend/src/app/components/accordion.rs create mode 100644 frontend/src/app/components/accordion_panel.rs create mode 100644 frontend/src/app/components/playlist/mapper_conter_view.rs create mode 100644 frontend/src/app/components/playlist/mapper_counter_view.rs diff --git a/backend/src/api/endpoints/xtream_api.rs b/backend/src/api/endpoints/xtream_api.rs index 6210e9db6..85db1ae4f 100644 --- a/backend/src/api/endpoints/xtream_api.rs +++ b/backend/src/api/endpoints/xtream_api.rs @@ -103,7 +103,7 @@ impl FromStr for ApiStreamContext { Self::TIMESHIFT => Ok(Self::Timeshift), _ => create_tuliprox_error_result!( TuliproxErrorKind::Info, - "Unknown CounterModifier: {}", + "Unknown ApiStreamContext: {}", s ), } diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index 0455afc11..a15778985 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -196,7 +196,9 @@ "INPUTS": "Inputs", "NEW_USER": "New User", "USERLIST": "Userlist", - "MAPPER": "Mapper" + "MAPPER": "Mapper", + "SCRIPT": "Script", + "COUNTER": "Counter" }, "TABLE": { "EMPTY": "", diff --git a/frontend/scss/_theme.scss b/frontend/scss/_theme.scss index 4341debbc..26eb16671 100644 --- a/frontend/scss/_theme.scss +++ b/frontend/scss/_theme.scss @@ -271,6 +271,30 @@ --menu-icon-color-main_config: #FF2DF1; --menu-icon-color-status: #F9E400; + --code-identifier-color: #569cd6; + --code-var-access-color: #9cdcfe; + --code-field-color: #bb8953; + --code-property-color: #9a652f; + --code-literal-color: #65a181; + --code-num-literal-color: #6796ba; + --code-built-in-function-color: #fac46c; + --code-reserved-color: #e98863; + --code-regex-color: #bb8953; + --code-enum-color: #65a181; + --code-range-color: #6796ba; + --code-op-color: #569cd6; + --code-comparison-background-color: #202429; + --code-comparison-color: #ffffff; + --code-bracket-1-color: #569CD6; + --code-bracket-2-color: #C586C0; + --code-bracket-3-color: #B5CEA8; + --code-editor-background-color: #1e1e1e; + --code-editor-color: #d4d4d4; + --code-any-match-color: #fa5f2d; + --code-null-literal-color: #fa5f2d; + + + --toolbar-bg-color: #161b25; } diff --git a/frontend/scss/app/_component.scss b/frontend/scss/app/_component.scss index 607dd1772..02500f879 100644 --- a/frontend/scss/app/_component.scss +++ b/frontend/scss/app/_component.scss @@ -49,6 +49,7 @@ @forward "components/custom_dialog"; @forward "components/filter"; @forward "components/mapper_script"; +@forward "components/mapper_counter"; @forward "components/userlist/user_state"; @forward "components/userlist/proxy_type"; @forward "components/userlist/max_connections"; @@ -58,3 +59,4 @@ @forward "components/loading_indicator"; @forward "components/toastr"; @forward "components/drop_down_icon_button"; +@forward "components/accordion"; diff --git a/frontend/scss/app/components/_accordion.scss b/frontend/scss/app/components/_accordion.scss new file mode 100644 index 000000000..617da9cde --- /dev/null +++ b/frontend/scss/app/components/_accordion.scss @@ -0,0 +1,7 @@ +.tp__accordion { + display: flex; + flex-flow: column; + overflow: hidden; + box-sizing: border-box; + width: 100%; +} \ No newline at end of file diff --git a/frontend/scss/app/components/_collapse_panel.scss b/frontend/scss/app/components/_collapse_panel.scss index 30aefdcce..d87263d74 100644 --- a/frontend/scss/app/components/_collapse_panel.scss +++ b/frontend/scss/app/components/_collapse_panel.scss @@ -2,7 +2,7 @@ display: flex; flex-flow: column; overflow: hidden; - flex-shrink: 0; + //flex-shrink: 0; &__header { display: flex; @@ -35,10 +35,10 @@ &__body { display: flex; flex-flow: column; - flex-shrink: 0; width: 100%; box-sizing: border-box; height: fit-content; + overflow: hidden; // transition: max-height 0.5s ease; // max-height: 5000px; } diff --git a/frontend/scss/app/components/_filter.scss b/frontend/scss/app/components/_filter.scss index 5e383c283..729b0976e 100644 --- a/frontend/scss/app/components/_filter.scss +++ b/frontend/scss/app/components/_filter.scss @@ -5,8 +5,8 @@ .tp__filter__code { font-family: monospace; - background: #1e1e1e; - color: #d4d4d4; + background: var(--code-editor-background-color); + color: var(--code-editor-color); padding: 1rem; border-radius: 8px; white-space: pre; @@ -14,38 +14,40 @@ .binary_op-wrapper{ white-space: pre; } + .comparison { white-space: nowrap; overflow: hidden; - background-color: #202429; + color: var(--code-comparison-color); + background-color: var(--code-comparison-background-color); padding: var(--padding-micro); border-radius: var(--border-radius); } .unary_op { font-weight: bold; - color: #569cd6; + color: var(--code-op-color); text-transform: uppercase; } .binary_op { font-weight: bold; - color: #9cdcfe; + color: var(--code-op-color); text-transform: uppercase; } .field { font-weight: bold; - color: #fdb76c; + color: var(--code-field-color); } .regex { - color: #bb8953; + color: var(--code-regex-color); white-space: nowrap; } .enum { - color: #78ce89; + color: var(--code-enum-color); } } @@ -54,12 +56,9 @@ font-size: 1.1rem; } - //.bracket-1 { color: #c551ac; } - //.bracket-2 { color: #70db39; } - //.bracket-3 { color: #dbdb02; } - .bracket-1 { color: #569CD6; } - .bracket-2 { color: #C586C0; } - .bracket-3 { color: #B5CEA8; } + .bracket-1 { color: var(--code-bracket-1-color); } + .bracket-2 { color: var(--code-bracket-2-color); } + .bracket-3 { color: var(--code-bracket-3-color); } } .tp__filter__inline { .tp__filter__code { diff --git a/frontend/scss/app/components/_mapper_counter.scss b/frontend/scss/app/components/_mapper_counter.scss new file mode 100644 index 000000000..c06afbd26 --- /dev/null +++ b/frontend/scss/app/components/_mapper_counter.scss @@ -0,0 +1,20 @@ +.tp__mapper-counter { + display: flex; + flex-flow: column; + overflow: auto; + box-sizing: border-box; + + &__row { + display: flex; + flex-flow: row; + box-sizing: border-box; + label { + color: var(--modest-text-color); + } + } +} + +.tp__mapper-counter__inline { + flex-flow: row; + overflow: hidden; +} \ No newline at end of file diff --git a/frontend/scss/app/components/_mapper_script.scss b/frontend/scss/app/components/_mapper_script.scss index f6153b803..bf0a913db 100644 --- a/frontend/scss/app/components/_mapper_script.scss +++ b/frontend/scss/app/components/_mapper_script.scss @@ -6,88 +6,58 @@ .tp__mapper-script__code { font-family: monospace; background: #1e1e1e; - color: #d4d4d4; - padding: 1rem; - border-radius: 8px; + color: var(--modest-text-color); + padding: var(--padding-default); + border-radius: var(--border-radius); white-space: pre; .identifier { - color: #569cd6; + color: var(--code-identifier-color); } .var-access { - color: #9cdcfe; + color: var(--code-var-access-color); } .field { - color: #fdb76c; + color: var(--code-field-color); } .property { - color: #9a652f; + color: var(--code-property-color); } .literal { - color: #5fcc5c; + color: var(--code-literal-color); } .num-literal { - color: #429140; + color: var(--code-num-literal-color); } - .null-literal { - border: 1px solid var(--border-color); - padding: var(--padding-micro); - border-radius: var(--border-radius); - color: var(--modest-text-color); - background-color: var(--card-background-color); - } .built-in-function { - + color: var(--code-built-in-function-color); } .reserved { - color: #da2ee0; + color: var(--code-reserved-color); } - //.binary_op-wrapper{ - // white-space: pre; - //} - // - //.comparison { - // white-space: nowrap; - // overflow: hidden; - // background-color: #202429; - // padding: var(--padding-micro); - // border-radius: var(--border-radius); - //} - // - //.unary_op { - // font-weight: bold; - // color: #569cd6; - // text-transform: uppercase; - //} - // - //.binary_op { - // font-weight: bold; - // color: #9cdcfe; - // text-transform: uppercase; - //} - // - //.field { - // font-weight: bold; - // color: #fdb76c; - //} - // - //.regex { - // color: #bb8953; - // white-space: nowrap; - //} - // - //.enum { - // color: #78ce89; - //} + .any-match { + color: var(--code-any-match-color); + font-weight: bold; + } + + .range { + color: var(--code-range-color); + font-weight: bold; + } + + .null-literal { + color: var(--code-null-literal-color); + font-weight: bold; + } } .bracket { diff --git a/frontend/scss/app/components/playlist/_playlist_mappings.scss b/frontend/scss/app/components/playlist/_playlist_mappings.scss index c351f2126..e6774fdcc 100644 --- a/frontend/scss/app/components/playlist/_playlist_mappings.scss +++ b/frontend/scss/app/components/playlist/_playlist_mappings.scss @@ -8,6 +8,14 @@ box-sizing: border-box; overflow: hidden; + &__list { + display: flex; + flex-flow: column; + gap: var(--gap-default); + box-sizing: border-box; + overflow: hidden; + } + &__mapping { display: flex; flex-flow: column; @@ -27,11 +35,7 @@ &-mapper { display: flex; - grid-template-columns: 1fr 1fr; - - @media (max-width: size.$dashboard-mobile-breakpoint) { - grid-template-columns: 1fr; - } + flex-flow: column; gap: var(--gap-default); box-sizing: border-box; @@ -44,6 +48,7 @@ justify-content: center; align-items: flex-start; box-sizing: border-box; + overflow: hidden; } } } diff --git a/frontend/scss/app/components/playlist/_source_selector.scss b/frontend/scss/app/components/playlist/_source_selector.scss index be83cc3da..b672328e1 100644 --- a/frontend/scss/app/components/playlist/_source_selector.scss +++ b/frontend/scss/app/components/playlist/_source_selector.scss @@ -1,6 +1,12 @@ @use "../../../size"; .tp__playlist-source-selector { + display: flex; + flex-flow: column; + gap: var(--gap-default); + box-sizing: border-box; + width: 100%; + flex-shrink: 0; &__source-picker .tp__collapse-panel__body { display: flex; diff --git a/frontend/src/app/components/accordion.rs b/frontend/src/app/components/accordion.rs new file mode 100644 index 000000000..cb13ce856 --- /dev/null +++ b/frontend/src/app/components/accordion.rs @@ -0,0 +1,31 @@ +use yew::prelude::*; +use std::rc::Rc; + +#[derive(Debug, Clone, PartialEq)] +pub struct AccordionState { + pub active_panel: Rc>>, +} + +#[derive(Properties, PartialEq)] +pub struct AccordionProps { + #[prop_or_default] + pub children: Children, + #[prop_or_default] + pub default_panel: Option, +} + +#[function_component] +pub fn Accordion(props: &AccordionProps) -> Html { + let active_panel = use_state(|| props.default_panel.clone()); + let state = AccordionState { + active_panel: Rc::new(active_panel), + }; + + html! { + context={state}> +

+ { for props.children.iter() } +
+ > + } +} diff --git a/frontend/src/app/components/accordion_panel.rs b/frontend/src/app/components/accordion_panel.rs new file mode 100644 index 000000000..fb7581ff3 --- /dev/null +++ b/frontend/src/app/components/accordion_panel.rs @@ -0,0 +1,48 @@ +use yew::prelude::*; +use crate::app::components::{AccordionState, AppIcon}; + +#[derive(Properties, Clone, PartialEq, Debug)] +pub struct AccordionPanelProps { + #[prop_or_default] + pub id: String, + #[prop_or_default] + pub title: String, + #[prop_or_default] + pub title_content: Option, + pub children: Children, + #[prop_or_default] + pub class: String, +} + +#[function_component] +pub fn AccordionPanel(props: &AccordionPanelProps) -> Html { + let context = use_context::().expect("AccordionPanel must be used inside Accordion"); + let expanded = (**context.active_panel).as_ref() == Some(&props.id); + + let toggle = { + let id = props.id.clone(); + let context = context.clone(); + + Callback::from(move |_| { + if (**context.active_panel).as_ref() == Some(&id) { + context.active_panel.set(None); + } else { + context.active_panel.set(Some(id.clone())); + } + }) + }; + + html! { +
+
+ + { props.title_content.clone().unwrap_or_else(|| html! { &props.title }) } + + +
+
+ { for props.children.iter() } +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/collapse_panel.rs b/frontend/src/app/components/collapse_panel.rs index f487528bb..0358378f2 100644 --- a/frontend/src/app/components/collapse_panel.rs +++ b/frontend/src/app/components/collapse_panel.rs @@ -24,18 +24,6 @@ pub fn CollapsePanel(props: &CollapsePanelProps) -> Html { Callback::from(move |_| expanded.set(!*expanded)) }; - // use_effect_with((expanded.clone(), panel_ref.clone()),move |(expanded, panel_ref)| { - // if let Some(element) = panel_ref.cast::() { - // if **expanded { - // let scroll_height = element.scroll_height(); - // element.style().set_property("height", &format!("{scroll_height}px")).unwrap(); - // } else { - // element.style().set_property("height", "0px").unwrap(); - // } - // } - // || () - // }); - html! {
diff --git a/frontend/src/app/components/mod.rs b/frontend/src/app/components/mod.rs index 1aa5fdc6c..334328dc1 100644 --- a/frontend/src/app/components/mod.rs +++ b/frontend/src/app/components/mod.rs @@ -30,7 +30,8 @@ mod search; mod loading_indicator; mod toastr; mod drop_down_icon_button; - +mod accordion; +mod accordion_panel; // pub use self::input::*; // pub use self::menu_item::*; // pub use self::toggle_switch::*; @@ -62,4 +63,6 @@ pub use self::radio_button_group::*; pub use self::no_content::*; pub use self::search::*; pub use self::toastr::*; -pub use self::drop_down_icon_button::*; \ No newline at end of file +pub use self::drop_down_icon_button::*; +pub use self::accordion::*; +pub use self::accordion_panel::*; \ No newline at end of file diff --git a/frontend/src/app/components/playlist/mapper_conter_view.rs b/frontend/src/app/components/playlist/mapper_conter_view.rs new file mode 100644 index 000000000..5249e6eec --- /dev/null +++ b/frontend/src/app/components/playlist/mapper_conter_view.rs @@ -0,0 +1,281 @@ +use std::ops::Deref; +use regex::Regex; +use shared::foundation::mapper::{MapperScript, Statement, Expression, ExprId, MapKey, BuiltInFunction, MapCase, AssignmentTarget, MatchCase, RegexSource, MapCaseKey}; +use yew::prelude::*; + +#[derive(Properties, PartialEq, Clone)] +pub struct MapperCounterViewProps { + #[prop_or_default] + pub pretty: bool, + #[prop_or(false)] + pub inline: bool, + pub script: Option, +} + +#[function_component] +pub fn MapperCounterView(props: &MapperScriptViewProps) -> Html { + html! { +
+ { + match props.script.as_ref() { + Some(script) => html! { +
+                            { render_script(script, props.pretty, 0, false, 1) }
+                        
+ }, + None => html! { }, + } + } +
+ } +} + +struct FormatParams { + pub pretty: bool, + pub level: usize, + pub do_indent: bool, + pub p_count: usize, +} + +impl FormatParams { + fn inc_level(&mut self, inc: usize) -> &mut Self { + self.level += inc; + self + } + fn dec_level(&mut self, dec: usize) -> &mut Self { + self.level -= dec; + self + } +} + +// Indents with spaces for pretty printing +fn indent(level: usize, do_indent: bool) -> Html { + if do_indent { + let spaces: AttrValue = " ".repeat(level * 2).into(); + html! { <>{ spaces } } + } else { + html! {} + } +} + +fn newline(format_params: &mut FormatParams) -> Html { + if format_params.pretty { + html! {
} + } else { + html!{} + } +} + +fn render_args(args: &[ExprId], script: &MapperScript, format_params: &mut FormatParams) -> Html { + html! { + <> + { + for args.iter().map(|expr_id| { + render_expression(expr_id, script, format_params) + }) + } + + } +} + +fn render_var_access(name: &str, field: &str) -> Html { + html! { { name }{ field } } +} + +fn render_field(field: &str) -> Html { + html! { {"@"}{ field } } +} + +fn render_identifier(ident: &str) -> Html { + html! { { ident } } +} + +fn render_map_key(key: &MapKey) -> Html { + match key { + MapKey::Identifier(ident) => render_identifier(ident), + MapKey::FieldAccess(field) => render_field(field), + MapKey::VarAccess(name, field) => render_var_access(name, field), + } +} + +fn render_function_call(name: &BuiltInFunction, args: &[ExprId], script: &MapperScript, format_params: &mut FormatParams) -> Html { + html! { + {name.to_string()}{"("}{render_args(args, script, format_params)}{")"} + } +} + +fn render_literal(literal: &str) -> Html { + html! { {"'"}{ literal }{"'"} } +} + +fn render_num_literal(literal: &f64) -> Html { + html! { { literal } } +} + +fn render_null_value() -> Html { + html! { { "null" } } +} + +fn render_map_case(case: &MapCase, script: &MapperScript, format_params: &mut FormatParams) -> Html { + let keys_html = html! { + <> + { + for case.keys.iter().enumerate().map(|(i, key)| { + let item = match key { + MapCaseKey::Text(text) => render_literal(text), + MapCaseKey::RangeFrom(from) => html! { format!("{from}..") }, + MapCaseKey::RangeTo(to) => html! { format!("..{to}") }, + MapCaseKey::RangeFull(from, to) => html! { format!("{from}..{to}") }, + MapCaseKey::RangeEq(val) => html! { val.to_string() }, + MapCaseKey::AnyMatch => html! { "_" }, + }; + + if i < case.keys.len() - 1 { + html! { <> { item } { ", " } } + } else { + html! { { item } } + } + }) + } + + }; + let has_bracket = case.keys.len() > 1; + html! { + <> + {indent(format_params.level, true)} + {if has_bracket {"("} else {""}} + {keys_html} + {if has_bracket {")"} else {""}} + {" => "} {render_expression(&case.expression, script, format_params)}{","} + {newline(format_params)} + + } +} + + +fn render_map_cases(cases: &[MapCase], script: &MapperScript, format_params: &mut FormatParams) -> Html { + html!{ + <> + { + for cases.iter().map(|case| render_map_case(case, script, format_params)) + } + + } +} + +fn render_map_block(map_key: &MapKey, cases: &[MapCase], script: &MapperScript, format_params: &mut FormatParams) -> Html { + html! { + <> + {indent(format_params.level, true)} + {"map "} + {render_map_key(map_key)} + {" {"} + {newline(format_params)} + {render_map_cases(cases, script, format_params.inc_level(1))} + {newline(format_params.dec_level(1))} + {indent(format_params.level, true)} + {"}"} + + } +} + + +fn render_block(expr_ids: &[ExprId], script: &MapperScript, format_params: &mut FormatParams) -> Html { + html! { + <> + {"{"} + { newline(format_params) } + { + for expr_ids.iter().map(|expr_id| { + html! { + <> + {render_expression(expr_id, script, format_params)} + {newline(format_params)} + + }}) + } + { newline(format_params) } + {"}"} + {newline(format_params)} + + + } +} + +fn render_assignment(target: &AssignmentTarget, expr_id: &ExprId, script: &MapperScript, format_params: &mut FormatParams) -> Html { + let target_html = match target { + AssignmentTarget::Identifier(ident) => render_identifier(ident), + AssignmentTarget::Field(field) => render_field(field), + }; + + html! { + <> + { target_html } {" = "} + { render_expression(expr_id, script, format_params) } + { newline(format_params) } + + } +} + +fn render_match_block(match_cases: &[MatchCase], script: &MapperScript, format_params: &mut FormatParams) -> Html { + html! { + <> + {"!! TODO MATCH BLOCK !!!"} + {newline(format_params)} + + } +} + + +fn render_regex_source(source: &RegexSource) -> Html { + match source { + RegexSource::Identifier(ident) => render_identifier(ident), + RegexSource::Field(field) => render_field(field), + } +} + +fn render_regexp(field: &RegexSource, pattern: &String, regex: &Regex) -> Html { + html! { <> {render_regex_source(field)} {" ~ "} {"'"}{ pattern }{"'"} } +} + +fn render_expression(expr_id: &ExprId, script: &MapperScript, format_params: &mut FormatParams) -> Html { + script.get_expr_by_id(*expr_id.deref()).map(|expression| { + match expression { + Expression::Identifier(ident) => render_identifier(ident), + Expression::StringLiteral(literal) => render_literal(literal), + Expression::NumberLiteral(num) => render_num_literal(num), + Expression::FieldAccess(field) => render_field(field), + Expression::VarAccess(name, field) => render_var_access(name, field), + Expression::RegexExpr { field, pattern, re_pattern } => render_regexp(field, pattern, re_pattern), + Expression::FunctionCall { name, args } => render_function_call(name, args, script, format_params), + Expression::Assignment { target, expr } => render_assignment(target, expr , script, format_params), + Expression::MatchBlock(match_cases) => render_match_block(match_cases, script, format_params), + Expression::MapBlock { key, cases} => render_map_block(key, cases, script, format_params), + Expression::NullValue => render_null_value(), + Expression::Block(expr_ids) => render_block(expr_ids, script, format_params), + } + }).unwrap_or_else(|| html! { {"ExprNotFound"} }) +} + +fn render_script(script: &MapperScript, pretty: bool, level: usize, do_indent: bool, p_count: usize) -> Html { + let mut format_params = FormatParams { + pretty, level, do_indent, p_count + }; + let items = script.statements.iter().map(|stmt| { + match stmt { + Statement::Expression(expr_id) => html! { + <> + {render_expression(expr_id, script, &mut format_params)} + {newline(&mut format_params)} + + }, + Statement::Comment(comment) => html!{
{ comment }
}, + } + }); + + html! { + <> + { for items } + + } +} diff --git a/frontend/src/app/components/playlist/mapper_counter_view.rs b/frontend/src/app/components/playlist/mapper_counter_view.rs new file mode 100644 index 000000000..2b7c44248 --- /dev/null +++ b/frontend/src/app/components/playlist/mapper_counter_view.rs @@ -0,0 +1,43 @@ +use std::sync::atomic::Ordering; +use yew::prelude::*; +use yew_i18n::use_translation; +use shared::model::MappingCounter; + +#[derive(Properties, PartialEq, Clone)] +pub struct MapperCounterViewProps { + #[prop_or_default] + pub pretty: bool, + #[prop_or(false)] + pub inline: bool, + pub counter: MappingCounter, +} + +#[function_component] +pub fn MapperCounterView(props: &MapperCounterViewProps) -> Html { + let translate = use_translation(); + + html! { +
+
+ + {props.counter.field.clone()} +
+
+ + {props.counter.concat.clone()} +
+
+ + {props.counter.modifier} +
+
+ + {props.counter.value.load(Ordering::Relaxed)} +
+
+ + {props.counter.padding} +
+
+ } +} diff --git a/frontend/src/app/components/playlist/mapper_script_view.rs b/frontend/src/app/components/playlist/mapper_script_view.rs index 2e221945a..105a9eca6 100644 --- a/frontend/src/app/components/playlist/mapper_script_view.rs +++ b/frontend/src/app/components/playlist/mapper_script_view.rs @@ -1,6 +1,6 @@ use std::ops::Deref; use regex::Regex; -use shared::foundation::mapper::{MapperScript, Statement, Expression, ExprId, MapKey, BuiltInFunction, MapCase, AssignmentTarget, MatchCase, RegexSource, MapCaseKey}; +use shared::foundation::mapper::{MapperScript, Statement, Expression, ExprId, MapKey, BuiltInFunction, MapCase, AssignmentTarget, MatchCase, RegexSource, MapCaseKey, MatchCaseKey}; use yew::prelude::*; #[derive(Properties, PartialEq, Clone)] @@ -123,11 +123,11 @@ fn render_map_case(case: &MapCase, script: &MapperScript, format_params: &mut Fo for case.keys.iter().enumerate().map(|(i, key)| { let item = match key { MapCaseKey::Text(text) => render_literal(text), - MapCaseKey::RangeFrom(from) => html! { format!("{from}..") }, - MapCaseKey::RangeTo(to) => html! { format!("..{to}") }, - MapCaseKey::RangeFull(from, to) => html! { format!("{from}..{to}") }, - MapCaseKey::RangeEq(val) => html! { val.to_string() }, - MapCaseKey::AnyMatch => html! { "_" }, + MapCaseKey::RangeFrom(from) => html! { {format!("{from}..")} }, + MapCaseKey::RangeTo(to) => html! { {format!("..{to}")} }, + MapCaseKey::RangeFull(from, to) => html! { {format!("{from}..{to}")} }, + MapCaseKey::RangeEq(val) => html! { {val.to_string()} }, + MapCaseKey::AnyMatch => html! { {"_"} }, }; if i < case.keys.len() - 1 { @@ -217,11 +217,60 @@ fn render_assignment(target: &AssignmentTarget, expr_id: &ExprId, script: &Mappe } } +fn render_match_case(case: &MatchCase, script: &MapperScript, format_params: &mut FormatParams) -> Html { + let keys_html = html! { + <> + { + for case.keys.iter().enumerate().map(|(i, key)| { + let item = match key { + MatchCaseKey::Identifier(text) => render_identifier(text), + MatchCaseKey::AnyMatch => html! { {"_"} }, + }; + + if i < case.keys.len() - 1 { + html! { <> { item } { ", " } } + } else { + html! { { item } } + } + }) + } + + }; + let has_bracket = case.keys.len() > 1; + html! { + <> + {indent(format_params.level, true)} + {if has_bracket {"("} else {""}} + {keys_html} + {if has_bracket {")"} else {""}} + {" => "} {render_expression(&case.expression, script, format_params)}{","} + {newline(format_params)} + + } +} + + +fn render_match_cases(cases: &[MatchCase], script: &MapperScript, format_params: &mut FormatParams) -> Html { + html!{ + <> + { + for cases.iter().map(|case| render_match_case(case, script, format_params)) + } + + } +} + fn render_match_block(match_cases: &[MatchCase], script: &MapperScript, format_params: &mut FormatParams) -> Html { html! { <> - {"!! TODO MATCH BLOCK !!!"} - {newline(format_params)} + {indent(format_params.level, true)} + {"match "} + {" {"} + {newline(format_params)} + {render_match_cases(match_cases, script, format_params.inc_level(1))} + {newline(format_params.dec_level(1))} + {indent(format_params.level, true)} + {"}"} } } @@ -234,8 +283,8 @@ fn render_regex_source(source: &RegexSource) -> Html { } } -fn render_regexp(field: &RegexSource, pattern: &String, regex: &Regex) -> Html { - html! { <> {render_regex_source(field)} {"~ '"}{ pattern } {"'"} } +fn render_regexp(field: &RegexSource, pattern: &String, _regex: &Regex) -> Html { + html! { <> {render_regex_source(field)} {" ~ "} {"'"}{ pattern }{"'"} } } fn render_expression(expr_id: &ExprId, script: &MapperScript, format_params: &mut FormatParams) -> Html { diff --git a/frontend/src/app/components/playlist/mappings.rs b/frontend/src/app/components/playlist/mappings.rs index 7e134e104..047234877 100644 --- a/frontend/src/app/components/playlist/mappings.rs +++ b/frontend/src/app/components/playlist/mappings.rs @@ -1,7 +1,7 @@ use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::{MapperDto, MappingCounterDefinition, MappingDto}; -use crate::app::components::{ConfigContext, FilterView, MapperScriptView, NoContent}; +use shared::model::{MapperDto, MappingCounter, MappingDto}; +use crate::app::components::{Accordion, AccordionPanel, ConfigContext, FilterView, MapperCounterView, MapperScriptView, NoContent}; use crate::app::components::toggle_switch::ToggleSwitch; #[derive(Properties, PartialEq, Clone)] @@ -38,11 +38,11 @@ pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html { } }; - let render_counter = |mapper: &MappingCounterDefinition| { + let render_counter = |counter: &MappingCounter| { html! {
- - + +
} }; @@ -58,28 +58,32 @@ pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html {
-
+ +
{ - for mapping.mapper.iter().flatten().map(|mapper| { - html! { render_mapper(mapper) } + for mapping.mapper.iter().flatten().enumerate().map(|(idx, mapper)| { + html! { + + { render_mapper(mapper) } + + } }) }
-
+
{ - for mapping.counter.iter().flatten().map(|counter| { - html! { render_counter(counter) } + for mapping.t_counter.iter().flatten().enumerate().map(|(idx, counter)| { + html! { + + { render_counter(counter) } + + } }) }
+
} - // pub mapper: Option>, - // pub counter: Option>, - // #[serde(skip_serializing, skip_deserializing)] - // pub t_counter: Option>, - // #[serde(skip_serializing, skip_deserializing)] - // pub templates: Option> }; html! { diff --git a/frontend/src/app/components/playlist/mod.rs b/frontend/src/app/components/playlist/mod.rs index e8ccab625..2d997e04b 100644 --- a/frontend/src/app/components/playlist/mod.rs +++ b/frontend/src/app/components/playlist/mod.rs @@ -16,6 +16,7 @@ mod source_selector; mod playlist_explorer; mod playlist_update_view; mod mapper_script_view; +mod mapper_counter_view; use std::rc::Rc; use yew_i18n::YewI18n; @@ -38,6 +39,7 @@ pub use self::playlist_editor_view::*; pub use self::playlist_explorer_view::*; pub use self::playlist_update_view::*; pub use self::mapper_script_view::*; +pub use self::mapper_counter_view::*; pub fn make_tags(data: &[(bool, &str)], translate: &YewI18n) -> Vec> { data.iter() From a3631e6443c596b1028cc18a79e9a743761ae9d4 Mon Sep 17 00:00:00 2001 From: euzu Date: Fri, 8 Aug 2025 12:04:14 +0200 Subject: [PATCH 6/6] CodeRabbit fixes --- README.md | 2 +- backend/src/api/model/provider_config.rs | 14 +++++-- .../src/app/components/playlist/mappings.rs | 41 ++++++++++--------- .../app/components/playlist/target_table.rs | 2 +- shared/src/model/config/web_auth.rs | 6 ++- 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index e6031b7b4..829cfde87 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,7 @@ log: - `enabled` can be deactivated if `enabled` is set to `false`. If not set default is `true`. - `issuer` - `secret` is used for jwt token generation. - - `token_ttl_mins` default 30 minutes, if set to 0 unlimited. + - `token_ttl_mins` default 30 minutes, setting it to 0 uses a 100-year expiration (effectively no expiration)—not recommended for production. !!CAUTION SECURITY RISK!!! - `userfile` is the file where the ui users are stored. If the filename is not absolute, `tuliprox` will look into the `config_dir`. If `userfile` is not given, the default value is `user.txt`. ```yaml web_ui: diff --git a/backend/src/api/model/provider_config.rs b/backend/src/api/model/provider_config.rs index 83cc06f89..1b4d7c23c 100644 --- a/backend/src/api/model/provider_config.rs +++ b/backend/src/api/model/provider_config.rs @@ -89,12 +89,18 @@ impl PartialEq for ProviderConfig { macro_rules! modify_connections { ($self:ident, $guard:ident, +1) => {{ - $guard.current_connections += 1; - $self.notify_connection_change($guard.current_connections); + let cnt = { + $guard.current_connections += 1; + $guard.current_connections + }; + $self.notify_connection_change(cnt); }}; ($self:ident, $guard:ident, -1) => {{ - $guard.current_connections -= 1; - $self.notify_connection_change($guard.current_connections); + let cnt = { + $guard.current_connections -= 1; + $guard.current_connections + }; + $self.notify_connection_change(cnt); }}; } diff --git a/frontend/src/app/components/playlist/mappings.rs b/frontend/src/app/components/playlist/mappings.rs index 047234877..03b4179a0 100644 --- a/frontend/src/app/components/playlist/mappings.rs +++ b/frontend/src/app/components/playlist/mappings.rs @@ -13,21 +13,24 @@ pub struct PlaylistMappingsProps { pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html { let translate = use_translation(); let config_ctx = use_context::().expect("Config context not found"); - let mappings = use_memo(config_ctx.clone(), |context| { - match &props.mappings { - Some(mapping_ids) => { - context.config.as_ref() - .and_then(|c| c.mappings.as_ref()) - .map(|mappings_dto| { - mappings_dto.mappings.mapping.iter() - .filter(|m| mapping_ids.contains(&m.id)) - .cloned() - .collect::>() - }) + let mappings = { + let ids = props.mappings.clone(); + use_memo((config_ctx.clone(), ids), |(context, mapping_ids)| { + match mapping_ids { + Some(ids) => { + context.config.as_ref() + .and_then(|c| c.mappings.as_ref()) + .map(|mappings_dto| { + mappings_dto.mappings.mapping.iter() + .filter(|m| ids.contains(&m.id)) + .cloned() + .collect::>() + }) + } + None => None, } - None => None, - } - }); + }) + }; let render_mapper = |mapper: &MapperDto| { html! { @@ -58,7 +61,7 @@ pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html {
- + }>
{ for mapping.mapper.iter().flatten().enumerate().map(|(idx, mapper)| { @@ -89,11 +92,9 @@ pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html { html! {
{ - match (*mappings).as_ref() { - Some(vec) => { - html! { for vec.iter().map(render_mapping) } - }, - None => html! { }, + match (*mappings).as_ref() { + Some(vec) if !vec.is_empty() => html! { for vec.iter().map(render_mapping) }, + _ => html! { }, } }
diff --git a/frontend/src/app/components/playlist/target_table.rs b/frontend/src/app/components/playlist/target_table.rs index 32e25360d..d17fdb250 100644 --- a/frontend/src/app/components/playlist/target_table.rs +++ b/frontend/src/app/components/playlist/target_table.rs @@ -101,7 +101,7 @@ pub fn TargetTable(props: &TargetTableProps) -> Html { 5 => dto.sort.as_ref().map_or_else(|| html! {}, |_s| html! { }), 6 => dto.t_filter.as_ref().map_or_else(|| html! {}, |f| html! { })}> }), 7 => dto.rename.as_ref().map_or_else(|| html! {}, |_r| html! { }), - 8 => html! { }, + 8 => html! { }, 9 => html! { }, 10 => html! { }, _ => html! {""}, diff --git a/shared/src/model/config/web_auth.rs b/shared/src/model/config/web_auth.rs index b9786b5b7..e88129a94 100644 --- a/shared/src/model/config/web_auth.rs +++ b/shared/src/model/config/web_auth.rs @@ -1,4 +1,8 @@ use crate::utils::{default_as_true}; +/// 30 minutes by default; `0` still means “no expiration.” +fn default_token_ttl_mins() -> u32 { + 30 +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] @@ -7,7 +11,7 @@ pub struct WebAuthConfigDto { pub enabled: bool, pub issuer: String, pub secret: String, - #[serde(default)] + #[serde(default = "default_token_ttl_mins")] pub token_ttl_mins: u32, #[serde(default, skip_serializing_if = "Option::is_none")] pub userfile: Option,