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/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/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/README.md b/README.md index 2a7f38e30..829cfde87 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, 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/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/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..1b4d7c23c 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 @@ -81,17 +89,23 @@ 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; + 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).await; + let cnt = { + $guard.current_connections -= 1; + $guard.current_connections + }; + $self.notify_connection_change(cnt); }}; } 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 +119,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 +137,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 +145,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 79312f528..d710dff3b 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(i64::from(duration))).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/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 98% rename from webui/public/assets/i18n/en.json rename to frontend/public/assets/i18n/en.json index d6f39d154..a15778985 100644 --- a/webui/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -195,7 +195,10 @@ "ALIAS": "Alias", "INPUTS": "Inputs", "NEW_USER": "New User", - "USERLIST": "Userlist" + "USERLIST": "Userlist", + "MAPPER": "Mapper", + "SCRIPT": "Script", + "COUNTER": "Counter" }, "TABLE": { "EMPTY": "", 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 92% rename from webui/scss/_theme.scss rename to frontend/scss/_theme.scss index 4341debbc..26eb16671 100644 --- a/webui/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/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 95% rename from webui/scss/app/_component.scss rename to frontend/scss/app/_component.scss index a85c7780a..02500f879 100644 --- a/webui/scss/app/_component.scss +++ b/frontend/scss/app/_component.scss @@ -48,6 +48,8 @@ @forward "components/dialog"; @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"; @@ -57,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/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 95% rename from webui/scss/app/components/_collapse_panel.scss rename to frontend/scss/app/components/_collapse_panel.scss index 30aefdcce..d87263d74 100644 --- a/webui/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/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 60% rename from webui/scss/app/components/_filter.scss rename to frontend/scss/app/components/_filter.scss index 5e383c283..729b0976e 100644 --- a/webui/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/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/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 new file mode 100644 index 000000000..bf0a913db --- /dev/null +++ b/frontend/scss/app/components/_mapper_script.scss @@ -0,0 +1,77 @@ +.tp__mapper-script { + display: flex; + flex-flow: column; + overflow: auto; + + .tp__mapper-script__code { + font-family: monospace; + background: #1e1e1e; + color: var(--modest-text-color); + padding: var(--padding-default); + border-radius: var(--border-radius); + white-space: pre; + + .identifier { + color: var(--code-identifier-color); + } + + .var-access { + color: var(--code-var-access-color); + } + + .field { + color: var(--code-field-color); + } + + .property { + color: var(--code-property-color); + } + + .literal { + color: var(--code-literal-color); + } + + .num-literal { + color: var(--code-num-literal-color); + } + + + .built-in-function { + color: var(--code-built-in-function-color); + } + + .reserved { + color: var(--code-reserved-color); + } + + .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 { + 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/_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..e6774fdcc --- /dev/null +++ b/frontend/scss/app/components/playlist/_playlist_mappings.scss @@ -0,0 +1,64 @@ +@use '../../../../scss/size'; + +.tp__playlist-mappings { + display: flex; + flex-flow: column; + justify-content: center; + align-items: flex-start; + 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; + 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; + flex-flow: column; + + 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; + overflow: hidden; + } + } + } + + 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 91% rename from webui/scss/app/components/playlist/_source_selector.scss rename to frontend/scss/app/components/playlist/_source_selector.scss index be83cc3da..b672328e1 100644 --- a/webui/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/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/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/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 71% rename from webui/src/app/components/collapse_panel.rs rename to frontend/src/app/components/collapse_panel.rs index f487528bb..0358378f2 100644 --- a/webui/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/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 93% rename from webui/src/app/components/custom_dialog.rs rename to frontend/src/app/components/custom_dialog.rs index cdfc45eb9..c7c49b1f2 100644 --- a/webui/src/app/components/custom_dialog.rs +++ b/frontend/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/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 90% rename from webui/src/app/components/mod.rs rename to frontend/src/app/components/mod.rs index 1aa5fdc6c..334328dc1 100644 --- a/webui/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/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/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 new file mode 100644 index 000000000..105a9eca6 --- /dev/null +++ b/frontend/src/app/components/playlist/mapper_script_view.rs @@ -0,0 +1,330 @@ +use std::ops::Deref; +use regex::Regex; +use shared::foundation::mapper::{MapperScript, Statement, Expression, ExprId, MapKey, BuiltInFunction, MapCase, AssignmentTarget, MatchCase, RegexSource, MapCaseKey, MatchCaseKey}; +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, +} + +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_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! { + <> + {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)} + {"}"} + + } +} + + +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/mappings.rs b/frontend/src/app/components/playlist/mappings.rs new file mode 100644 index 000000000..03b4179a0 --- /dev/null +++ b/frontend/src/app/components/playlist/mappings.rs @@ -0,0 +1,102 @@ +use yew::prelude::*; +use yew_i18n::use_translation; +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)] +pub struct PlaylistMappingsProps { + pub mappings: Option>, +} + +#[function_component] +pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("Config context not found"); + 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, + } + }) + }; + + let render_mapper = |mapper: &MapperDto| { + html! { +
+ + +
+ } + }; + + let render_counter = |counter: &MappingCounter| { + html! { +
+ + +
+ } + }; + + let render_mapping = |mapping: &MappingDto| { + html! { +
+
+ + {mapping.id.clone()} +
+
+ + +
+ }> +
+ { + for mapping.mapper.iter().flatten().enumerate().map(|(idx, mapper)| { + html! { + + { render_mapper(mapper) } + + } + }) + } +
+
+ { + for mapping.t_counter.iter().flatten().enumerate().map(|(idx, counter)| { + html! { + + { render_counter(counter) } + + } + }) + } +
+
+
+ } + }; + + html! { +
+ { + match (*mappings).as_ref() { + Some(vec) if !vec.is_empty() => html! { for vec.iter().map(render_mapping) }, + _ => html! { }, + } + } +
+ } +} \ No newline at end of file diff --git a/webui/src/app/components/playlist/mod.rs b/frontend/src/app/components/playlist/mod.rs similarity index 90% rename from webui/src/app/components/playlist/mod.rs rename to frontend/src/app/components/playlist/mod.rs index e608f0ae9..2d997e04b 100644 --- a/webui/src/app/components/playlist/mod.rs +++ b/frontend/src/app/components/playlist/mod.rs @@ -15,6 +15,8 @@ mod input; 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; @@ -36,6 +38,8 @@ 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 use self::mapper_counter_view::*; pub fn make_tags(data: &[(bool, &str)], translate: &YewI18n) -> Vec> { data.iter() 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 97% rename from webui/src/app/components/playlist/target_table.rs rename to frontend/src/app/components/playlist/target_table.rs index 32e25360d..d17fdb250 100644 --- a/webui/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/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 81% rename from webui/src/services/config_service.rs rename to frontend/src/services/config_service.rs index 819013adb..3d7ef878b 100644 --- a/webui/src/services/config_service.rs +++ b/frontend/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) => { 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/shared/src/foundation/mapper.rs b/shared/src/foundation/mapper.rs index 054c10475..82332db66 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, @@ -125,19 +135,37 @@ 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}"), } } } +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/shared/src/model/config/web_auth.rs b/shared/src/model/config/web_auth.rs index db1513535..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,6 +11,8 @@ pub struct WebAuthConfigDto { pub enabled: bool, pub issuer: String, pub secret: String, + #[serde(default = "default_token_ttl_mins")] + pub token_ttl_mins: u32, #[serde(default, skip_serializing_if = "Option::is_none")] pub userfile: Option, } diff --git a/webui/public/assets/favicon.ico b/webui/public/assets/favicon.ico deleted file mode 100644 index 8d861c4c8..000000000 Binary files a/webui/public/assets/favicon.ico and /dev/null differ 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 fca1c231b..000000000 Binary files a/webui/public/assets/fonts/rubik-arabic-400-italic.woff and /dev/null differ 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 b42c25f4f..000000000 Binary files a/webui/public/assets/fonts/rubik-arabic-400-italic.woff2 and /dev/null differ 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 3b48a3e5e..000000000 Binary files a/webui/public/assets/fonts/rubik-arabic-400-normal.woff and /dev/null differ 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 673f79567..000000000 Binary files a/webui/public/assets/fonts/rubik-arabic-400-normal.woff2 and /dev/null differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff b/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff deleted file mode 100644 index b11f2dc6d..000000000 Binary files a/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff and /dev/null differ 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 d6d59de0a..000000000 Binary files a/webui/public/assets/fonts/rubik-cyrillic-400-italic.woff2 and /dev/null differ 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 0506d064a..000000000 Binary files a/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff and /dev/null differ diff --git a/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff2 b/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff2 deleted file mode 100644 index 8a166d4df..000000000 Binary files a/webui/public/assets/fonts/rubik-cyrillic-400-normal.woff2 and /dev/null differ 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 42098f8a8..000000000 Binary files a/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff and /dev/null differ 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 109a7ca52..000000000 Binary files a/webui/public/assets/fonts/rubik-cyrillic-ext-400-italic.woff2 and /dev/null differ 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 d47ad615c..000000000 Binary files a/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff and /dev/null differ 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 cca969e32..000000000 Binary files a/webui/public/assets/fonts/rubik-cyrillic-ext-400-normal.woff2 and /dev/null differ 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 cb34551ba..000000000 Binary files a/webui/public/assets/fonts/rubik-hebrew-400-italic.woff and /dev/null differ diff --git a/webui/public/assets/fonts/rubik-hebrew-400-italic.woff2 b/webui/public/assets/fonts/rubik-hebrew-400-italic.woff2 deleted file mode 100644 index 798e805e4..000000000 Binary files a/webui/public/assets/fonts/rubik-hebrew-400-italic.woff2 and /dev/null differ 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 df2536016..000000000 Binary files a/webui/public/assets/fonts/rubik-hebrew-400-normal.woff and /dev/null differ 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 658d71941..000000000 Binary files a/webui/public/assets/fonts/rubik-hebrew-400-normal.woff2 and /dev/null differ 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 458d6cd9f..000000000 Binary files a/webui/public/assets/fonts/rubik-latin-400-italic.woff and /dev/null differ 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 af50274a4..000000000 Binary files a/webui/public/assets/fonts/rubik-latin-400-italic.woff2 and /dev/null differ 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 25a12df47..000000000 Binary files a/webui/public/assets/fonts/rubik-latin-400-normal.woff and /dev/null differ 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 ac6ff6829..000000000 Binary files a/webui/public/assets/fonts/rubik-latin-400-normal.woff2 and /dev/null differ diff --git a/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff b/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff deleted file mode 100644 index c7530972a..000000000 Binary files a/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff and /dev/null differ 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 0d4e484ff..000000000 Binary files a/webui/public/assets/fonts/rubik-latin-ext-400-italic.woff2 and /dev/null differ diff --git a/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff b/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff deleted file mode 100644 index a97a610d3..000000000 Binary files a/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff and /dev/null differ 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 f8a84d783..000000000 Binary files a/webui/public/assets/fonts/rubik-latin-ext-400-normal.woff2 and /dev/null differ 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 d157b8f4e..000000000 Binary files a/webui/public/assets/placeholder.png and /dev/null differ 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 c0b8e1691..000000000 --- a/webui/scss/app/components/playlist/_playlist_mappings.scss +++ /dev/null @@ -1,20 +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) - } - } - -} \ No newline at end of file diff --git a/webui/src/app/components/playlist/mappings.rs b/webui/src/app/components/playlist/mappings.rs deleted file mode 100644 index 68ac45c88..000000000 --- a/webui/src/app/components/playlist/mappings.rs +++ /dev/null @@ -1,64 +0,0 @@ -use yew::prelude::*; -use yew_i18n::use_translation; -use shared::model::MappingDto; -use crate::app::components::{ConfigContext, NoContent}; - -#[derive(Properties, PartialEq, Clone)] -pub struct PlaylistMappingsProps { - pub mappings: Option>, -} - -#[function_component] -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::>() - }) - } - None => None, - } - }); - - let render_mapping = |mapping: &MappingDto| { - html! { -
-
- - {mapping.id.clone()} -
-
- } - // - // pub id: String, - // #[serde(default)] - // pub match_as_ascii: bool, - // pub mapper: Option>, - // pub counter: Option>, - // #[serde(skip_serializing, skip_deserializing)] - // pub t_counter: Option>, - // #[serde(skip_serializing, skip_deserializing)] - // pub templates: Option> - }; - - html! { -
- { - match (*mappings).as_ref() { - Some(vec) => { - html! { for vec.iter().map(render_mapping) } - }, - None => html! { }, - } - } -
- } -} \ No newline at end of file