diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..7e96f5cf8 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + +# Avoid running the same CI multiple times for the same PR +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Job 1: Check code style (very fast) + style: + name: Format & Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Nightly Rust + uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt, clippy + + - name: Check Formatting + # This now works with cargo +nightly fmt + run: make fmt-check + + - name: Lint with Clippy + run: make lint + + # Job 2: Build and Test (takes longer) + test: + name: Build & Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Stable Rust + uses: dtolnay/rust-toolchain@stable + + - name: Rust Cache + # This magic step saves compiled dependencies between runs + uses: Swatinem/rust-cache@v2 + + - name: Run Tests + run: make test diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 4b1653936..7c1547e81 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,8 +1,5 @@ name: Docker Build and Push -run-name: >- - ${{ github.workflow }}${{ github.event_name == 'workflow_dispatch' && format(' [{0}] {1}:{2}', inputs.run_key, inputs.branch, inputs.choice) || '' }} - permissions: contents: write packages: write @@ -12,266 +9,130 @@ on: inputs: branch: type: choice - description: Target branch to build/push for - required: true + description: Target branch + options: [develop, master] default: develop - options: - - develop - - master choice: type: choice - description: Trigger build and optionally bump version - required: true + description: Version bump + options: [none, major, minor, patch] default: none - options: - - none - - major - - minor - - patch - run_key: - type: string - description: Correlation id (optional) - required: false - default: "" push: - branches: - - develop + branches: [develop] + +# Avoid multiple parallel builds for the same branch to prevent race conditions +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false # Set to true if you want to abort old builds immediately jobs: docker-build: runs-on: ubuntu-latest if: >- - ( - github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/develop' - ) && ( - github.event_name != 'push' || github.actor != 'github-actions[bot]' - ) && ( - github.event_name != 'push' || - github.event.head_commit.message == null || - !startsWith(github.event.head_commit.message, 'ci: bump version v') - ) + (github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/develop') && + (github.event_name != 'push' || github.actor != 'github-actions[bot]') steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch || github.ref }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + # Ensure we always have the right branch context + ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || github.ref }} + + - name: Install Rust Tools (Binaries) + uses: taiki-e/install-action@v2 with: - platforms: linux/amd64,linux/arm64 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64,linux/arm64 - - - name: Create and use multi-platform builder - run: | - docker buildx create --name multiarch --use --bootstrap - docker buildx inspect multiarch - - - name: Install Rust + tool: cross,trunk,wasm-bindgen-cli,cargo-edit + + - name: Install Rust Toolchain uses: dtolnay/rust-toolchain@stable with: targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl,wasm32-unknown-unknown - - name: Detect runner image/libc - id: runner_env - shell: bash - run: | - set -euo pipefail - echo "image_os=${ImageOS:-unknown}" >> "$GITHUB_OUTPUT" - echo "image_version=${ImageVersion:-unknown}" >> "$GITHUB_OUTPUT" - if command -v ldd >/dev/null 2>&1; then - glibc="$(ldd --version 2>/dev/null | head -n1 | awk '{print $NF}')" - else - glibc="unknown" - fi - echo "glibc=${glibc:-unknown}" >> "$GITHUB_OUTPUT" - - - name: Cache cargo tools - id: cache-tools - uses: actions/cache@v4 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 with: - path: | - ~/.cargo/bin - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-${{ steps.runner_env.outputs.image_os }}-${{ steps.runner_env.outputs.image_version }}-${{ steps.runner_env.outputs.glibc }}-cargo-tools-v2 - restore-keys: | - ${{ runner.os }}-${{ steps.runner_env.outputs.image_os }}-${{ steps.runner_env.outputs.image_version }}-${{ steps.runner_env.outputs.glibc }}-cargo-tools- + cache-targets: "true" - - name: Install cross - run: | - if ! command -v cross &> /dev/null; then - cargo install cross --git https://github.com/cross-rs/cross - fi + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - name: Install trunk - run: | - if ! command -v trunk &> /dev/null; then - RUSTFLAGS="" cargo install trunk --locked - fi + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Install wasm-bindgen-cli - run: | - if ! command -v wasm-bindgen &> /dev/null; then - cargo install wasm-bindgen-cli - fi - - - name: Install cargo-edit - run: | - if ! command -v cargo-set-version &> /dev/null; then - cargo install cargo-edit --locked - fi - - - name: Cache cargo registry and dependencies - id: cache-cargo-deps - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git/db - key: ${{ runner.os }}-${{ steps.runner_env.outputs.image_os }}-${{ steps.runner_env.outputs.image_version }}-${{ steps.runner_env.outputs.glibc }}-cargo-deps-v2-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/Cargo.toml') }} - restore-keys: | - ${{ runner.os }}-${{ steps.runner_env.outputs.image_os }}-${{ steps.runner_env.outputs.image_version }}-${{ steps.runner_env.outputs.glibc }}-cargo-deps-v2-${{ hashFiles('**/Cargo.lock') }}- - ${{ runner.os }}-${{ steps.runner_env.outputs.image_os }}-${{ steps.runner_env.outputs.image_version }}-${{ steps.runner_env.outputs.glibc }}-cargo-deps-v2- - ${{ runner.os }}-cargo-deps- - - - name: Cache Docker buildx - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Cache frontend dist - id: cache-frontend - uses: actions/cache@v4 - with: - path: frontend/dist - key: ${{ runner.os }}-frontend-${{ hashFiles('frontend/**/*', 'shared/**/*') }}-${{ hashFiles('frontend/Cargo.toml', 'frontend/Trunk.toml') }} - restore-keys: | - ${{ runner.os }}-frontend- - - - name: Cache cross-compilation tools - uses: actions/cache@v4 - with: - path: | - ~/.cross - ~/.rustup/toolchains/*/lib/rustlib/x86_64-unknown-linux-musl - ~/.rustup/toolchains/*/lib/rustlib/aarch64-unknown-linux-musl - key: ${{ runner.os }}-cross-${{ hashFiles('**/Cargo.lock') }}-v2 - restore-keys: | - ${{ runner.os }}-cross- - - name: Cache built resources id: cache-resources uses: actions/cache@v4 with: - path: | - resources/*.ts - key: ${{ runner.os }}-resources-${{ hashFiles('resources/*.jpg') }}-v1 - restore-keys: | - ${{ runner.os }}-resources- - - - name: Install ffmpeg (only if resources not cached) + path: resources/*.ts + key: ${{ runner.os }}-resources-${{ hashFiles('resources/*.jpg') }} + + - name: Install ffmpeg if: steps.cache-resources.outputs.cache-hit != 'true' run: | sudo apt-get update sudo apt-get install -y ffmpeg - - name: Bump patch version (develop) - if: github.event_name == 'push' && github.ref == 'refs/heads/develop' - run: ./bin/inc_version.sh - - - name: Set version (manual) - if: github.event_name == 'workflow_dispatch' - shell: bash + - name: Build TS resources + if: steps.cache-resources.outputs.cache-hit != 'true' run: | - CHOICE='${{ github.event.inputs.choice }}' - case "${CHOICE:-none}" in - none) echo "Skipping version bump" ;; - major) ./bin/inc_version.sh m ;; - minor) ./bin/inc_version.sh p ;; - patch) ./bin/inc_version.sh ;; - *) echo "🧨 Error: Unsupported option"; exit 1 ;; - esac + chmod +x ./bin/build_resources.sh + ./bin/build_resources.sh - - name: Commit version bump (local) - id: version_commit - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - shell: bash + - name: Verify TS resources run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - FILES=(Cargo.lock backend/Cargo.lock backend/Cargo.toml frontend/Cargo.toml shared/Cargo.toml) - for f in "${FILES[@]}"; do - if [ -f "$f" ]; then - git add "$f" + required_resources=( + "channel_unavailable" + "user_connections_exhausted" + "provider_connections_exhausted" + "user_account_expired" + "panel_api_provisioning" + ) + for resource in "${required_resources[@]}"; do + if [[ ! -f "resources/${resource}.ts" ]]; then + echo "🧨 Missing resource: resources/${resource}.ts" + exit 1 fi done + - name: Handle Version Bump + run: | + chmod +x ./bin/inc_version.sh + if [[ "${{ github.event_name }}" == "push" ]]; then + ./bin/inc_version.sh + else + case "${{ github.event.inputs.choice }}" in + major) ./bin/inc_version.sh m ;; + minor) ./bin/inc_version.sh p ;; + patch) ./bin/inc_version.sh ;; + esac + fi + + - name: Commit version bump + id: version_commit + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add . if git diff --cached --quiet; then echo "did_commit=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - VERSION="$(grep '^version' backend/Cargo.toml | head -n1 | cut -d'"' -f2)" - if [ -n "${VERSION}" ]; then - git commit -m "ci: bump version v${VERSION}" else - git commit -m "ci: bump version" + VERSION=$(grep '^version' backend/Cargo.toml | head -n1 | cut -d'"' -f2) + git commit -m "ci: bump version v$VERSION" + echo "did_commit=true" >> "$GITHUB_OUTPUT" fi - echo "did_commit=true" >> "$GITHUB_OUTPUT" - - - name: Extract branch name - shell: bash - id: extract_branch - run: | - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - echo "branch=${{ inputs.branch }}" >> "$GITHUB_OUTPUT" - else - echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT" - fi - - - name: Make build script executable - run: chmod +x ./bin/build_docker.sh - name: Build and push Docker images env: GITHUB_IO_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO_OWNER: ${{ github.repository_owner }} - FRONTEND_CACHE_HIT: ${{ steps.cache-frontend.outputs.cache-hit }} - CARGO_DEPS_CACHE_HIT: ${{ steps.cache-cargo-deps.outputs.cache-hit }} - BUILDX_CACHE_FROM: type=local,src=/tmp/.buildx-cache - BUILDX_CACHE_TO: type=local,dest=/tmp/.buildx-cache-new,mode=max - run: ./bin/build_docker.sh ${{ steps.extract_branch.outputs.branch }} + run: | + chmod +x ./bin/build_docker.sh + ./bin/build_docker.sh ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || 'develop' }} - name: Push version bump if: success() && steps.version_commit.outputs.did_commit == 'true' - shell: bash run: | - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - BRANCH='${{ inputs.branch }}' - else - BRANCH="${GITHUB_REF#refs/heads/}" - fi - - if [[ "${BRANCH}" != "develop" && "${BRANCH}" != "master" ]]; then - echo "Skipping push: unsupported branch '${BRANCH}'" - exit 0 - fi - - git push origin "HEAD:${BRANCH}" - - - name: Move buildx cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache || true + # Use origin HEAD to push to the correct remote branch + git push origin HEAD diff --git a/.gitignore b/.gitignore index 474a021a5..a750ff0bd 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ docker/resources /.vscode .claude +.envrc \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..b04cf6179 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +Thank you for your interest in contributing to Tuliprox! We welcome contributions of all kinds, including bug fixes, new features, documentation improvements, and more. To ensure a smooth contribution process, please follow the guidelines outlined below. + +## How to Contribute + +### Prerequisites + +To contribute you will need Rust, cargo, cross, trunk, wasm-bindgen, and cargo-set-version installed. You can install them using the following command: + +```bash +make install-tools +``` + +### Steps to Contribute + +1. **Fork the Repository**: Start by forking the Tuliprox repository to your GitHub account. +2. **Clone Your Fork**: Clone the forked repository to your local machine. + + ```bash + git clone git@github.com:your-username/tuliprox.git + cd tuliprox + ``` + +3. **Create a Branch**: Create a new branch for your contribution. + + ```bash + git checkout -b feature/your-feature-name + ``` + +4. **Make Your Changes**: Implement your changes in the codebase. Please ensure that your code follows the existing style and conventions. +5. **Test Your Changes**: Format, lint, and run tests to ensure that your changes are clean and tidy and do not break existing functionality. + + ```bash + make fmt lint test + ``` + +6. **Commit Your Changes**: Commit your changes with a descriptive message. + + ```bash + git add . + git commit -m "Add your descriptive message here" + ``` + +7. **Push Your Changes**: Push your changes to your forked repository. + + ```bash + git push origin feature/your-feature-name + ``` + +8. **Create a Pull Request**: Open a pull request on the original Tuliprox repository, describing your changes and their purpose. +9. **Address Feedback**: Be responsive to any feedback or requests for changes from the maintainers. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..a0b49b43e --- /dev/null +++ b/Makefile @@ -0,0 +1,71 @@ +# --- Setup and OS Detection --- +OS := $(shell uname -s) +ARCH := $(shell uname -m) + +# Tool Commands +CARGO := cargo +RUSTUP := rustup +# Explicitly force stable/nightly to avoid system-wide overrides +CARGO_STABLE := $(CARGO) +stable +CARGO_NIGHTLY := $(CARGO) +nightly +CROSS := cross +TRUNK := trunk + +# Colors for terminal output +AQUA := \033[36m +RESET := \033[0m +BOLD := \033[1m + +.DEFAULT_GOAL := help + +.PHONY: help +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make $(AQUA)$(RESET)\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " $(AQUA)%-26s$(RESET) %s\n", $$1, $$2 } /^##@/ { printf "\n$(BOLD)%s$(RESET)\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Prerequisites: + +.PHONY: install-tools +install-tools: check-rustup install-nightly-fmt ## Install all required development tools + @echo "πŸ“¦ Checking and installing CLI tools..." + @command -v cross >/dev/null || $(CARGO_STABLE) install cross + @command -v trunk >/dev/null || $(CARGO_STABLE) install trunk + @command -v wasm-bindgen >/dev/null || $(CARGO_STABLE) install wasm-bindgen-cli + @command -v cargo-set-version >/dev/null || $(CARGO_STABLE) install cargo-edit + @echo "βœ… All tools installed" + +.PHONY: check-rustup +check-rustup: ## Ensure rustup is installed + @command -v $(RUSTUP) >/dev/null || (echo "❌ rustup not found. Please install it from https://rustup.rs"; exit 1) + +.PHONY: install-nightly-fmt +install-nightly-fmt: ## Install nightly toolchain specifically for formatting + @echo "πŸ“¦ Ensuring nightly rustfmt is available" + @$(RUSTUP) toolchain install nightly --component rustfmt --profile minimal + @echo "βœ… Nightly rustfmt ready" + +##@ Development: + +.PHONY: test +test: ## Run all workspace tests (Stable) + @echo "==> Running tests (stable)" + $(CARGO_STABLE) test --workspace + +.PHONY: lint +lint: ## Run clippy linter (Stable) + @echo "==> Running clippy (stable)" + $(CARGO_STABLE) clippy --workspace -- -D warnings + +.PHONY: lint-fix +lint-fix: ## Automatically fix clippy suggestions (Stable) + @echo "==> Applying clippy auto-fixes" + $(CARGO_STABLE) clippy --fix --workspace --allow-dirty --allow-staged -- -D clippy::uninlined_format_args + +.PHONY: fmt +fmt: ## Format all code using nightly rules (Compact) + @echo "==> Formatting code (nightly)" + $(CARGO_NIGHTLY) fmt --all + +.PHONY: fmt-check +fmt-check: ## Check if code follows formatting rules (Nightly) + @echo "==> Checking formatting (nightly)" + $(CARGO_NIGHTLY) fmt --all -- --check diff --git a/backend/src/api/model/metadata_update_manager.rs b/backend/src/api/model/metadata_update_manager.rs index f176ad19d..b60b88058 100644 --- a/backend/src/api/model/metadata_update_manager.rs +++ b/backend/src/api/model/metadata_update_manager.rs @@ -277,12 +277,12 @@ impl MetadataUpdateManager { (UpdateTask::ResolveVod { reason: r1, delay: d1, .. }, UpdateTask::ResolveVod { reason: r2, delay: d2, .. }) | (UpdateTask::ResolveSeries { reason: r1, delay: d1, .. }, UpdateTask::ResolveSeries { reason: r2, delay: d2, .. }) | (UpdateTask::ProbeStream { reason: r1, delay: d1, .. }, UpdateTask::ProbeStream { reason: r2, delay: d2, .. }) => { - *r1 = *r1 | r2; + *r1 |= r2; *d1 = min(*d1, d2); merged = true; } (UpdateTask::ProbeLive { reason: r1, delay: d1, interval: i1, .. }, UpdateTask::ProbeLive { reason: r2, delay: d2, interval: i2, .. }) => { - *r1 = *r1 | r2; + *r1 |= r2; *d1 = min(*d1, d2); *i1 = min(*i1, i2); merged = true; diff --git a/backend/src/main.rs b/backend/src/main.rs index df7eda176..bbfc17d19 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -16,23 +16,19 @@ mod modules; include_modules!(); -use crate::auth::generate_password; -use crate::library::LibraryProcessor; -use crate::model::{ - AppConfig, Config, Healthcheck, HealthcheckConfig, ProcessTargets, SourcesConfig, +use crate::{ + auth::generate_password, + library::LibraryProcessor, + model::{AppConfig, Config, Healthcheck, HealthcheckConfig, ProcessTargets, SourcesConfig}, + processing::processor::exec_processing, + utils::{config_file_reader, db_viewer, init_logger, request::create_client, resolve_env_var}, }; -use crate::processing::processor::{exec_processing}; -use crate::utils::request::create_client; -use crate::utils::{config_file_reader, resolve_env_var}; -use crate::utils::{db_viewer, init_logger}; -use arc_swap::access::Access; -use arc_swap::ArcSwap; +use arc_swap::{access::Access, ArcSwap}; use chrono::{DateTime, Utc}; use clap::Parser; use log::{error, info, warn}; use shared::model::ConfigPaths; -use std::fs::File; -use std::sync::Arc; +use std::{fs::File, sync::Arc}; #[allow(clippy::struct_excessive_bools)] #[derive(Parser)] @@ -66,12 +62,7 @@ struct Args { api_proxy: Option, /// Run in server mode - #[arg( - short = 's', - long, - default_value_t = false, - default_missing_value = "true" - )] + #[arg(short = 's', long, default_value_t = false, default_missing_value = "true")] server: bool, /// log level @@ -121,10 +112,12 @@ const BUILD_TIMESTAMP: &str = env!("VERGEN_BUILD_TIMESTAMP"); async fn main() { let args = Args::parse(); - db_viewer(args.db_xtream_file_name.as_deref(), - args.db_m3u_file_name.as_deref(), - args.db_epg_file_name.as_deref(), - args.db_tim_file_name.as_deref()); + db_viewer( + args.db_xtream_file_name.as_deref(), + args.db_m3u_file_name.as_deref(), + args.db_epg_file_name.as_deref(), + args.db_tim_file_name.as_deref(), + ); if args.genpwd { match generate_password() { @@ -136,10 +129,7 @@ async fn main() { let mut config_paths = get_file_paths(&args); - init_logger( - args.log_level.as_deref(), - config_paths.config_file_path.as_str(), - ); + init_logger(args.log_level.as_deref(), config_paths.config_file_path.as_str()); if args.healthcheck { let healthy = healthcheck(config_paths.config_file_path.as_str()).await; @@ -149,7 +139,11 @@ async fn main() { // Handle Library scan before starting main application if args.scan_library || args.force_library_rescan { info!("Library scan mode requested"); - let app_config = Arc::new(utils::read_initial_app_config(&mut config_paths, true, true, false).await.unwrap_or_else(|err| exit!("{}", err))); + let app_config = Arc::new( + utils::read_initial_app_config(&mut config_paths, true, true, false) + .await + .unwrap_or_else(|err| exit!("{}", err)), + ); scan_library_cli(&app_config, args.force_library_rescan).await; return; } @@ -169,9 +163,7 @@ async fn main() { print_info(&app_config); let sources = > as Access>::load(&app_config.sources); - let targets = sources - .validate_targets(args.target.as_ref()) - .unwrap_or_else(|err| exit!("{err}")); + let targets = sources.validate_targets(args.target.as_ref()).unwrap_or_else(|err| exit!("{err}")); if args.server { start_in_server_mode(Arc::new(app_config), Arc::new(targets)).await; @@ -183,10 +175,7 @@ async fn main() { fn print_info(app_config: &AppConfig) { let config = > as Access>::load(&app_config.config); let paths = > as Access>::load(&app_config.paths); - info!( - "Current time: {}", - chrono::offset::Local::now().format("%Y-%m-%d %H:%M:%S") - ); + info!("Current time: {}", chrono::offset::Local::now().format("%Y-%m-%d %H:%M:%S")); info!("Temp dir: {}", tempfile::env::temp_dir().display()); info!("Working dir: {:?}", &config.working_dir); info!("Config dir: {:?}", &paths.config_path); @@ -217,10 +206,27 @@ fn print_info(app_config: &AppConfig) { } fn get_file_paths(args: &Args) -> ConfigPaths { - let config_path: String = utils::resolve_directory_path(&resolve_env_var(&args.config_path.as_ref().map_or_else(utils::get_default_config_path, ToString::to_string))); - let config_file: String = resolve_env_var(&args.config_file.as_ref().map_or_else(|| utils::get_default_config_file_path(&config_path), ToString::to_string)); - let api_proxy_file = resolve_env_var(&args.api_proxy.as_ref().map_or_else(|| utils::get_default_api_proxy_config_path(config_path.as_str()), ToString::to_string)); - let sources_file: String = resolve_env_var(&args.source_file.as_ref().map_or_else(|| utils::get_default_sources_file_path(&config_path), ToString::to_string)); + let config_path: String = utils::resolve_directory_path(&resolve_env_var( + &args.config_path.as_ref().map_or_else(utils::get_default_config_path, ToString::to_string), + )); + let config_file: String = resolve_env_var( + &args + .config_file + .as_ref() + .map_or_else(|| utils::get_default_config_file_path(&config_path), ToString::to_string), + ); + let api_proxy_file = resolve_env_var( + &args + .api_proxy + .as_ref() + .map_or_else(|| utils::get_default_api_proxy_config_path(config_path.as_str()), ToString::to_string), + ); + let sources_file: String = resolve_env_var( + &args + .source_file + .as_ref() + .map_or_else(|| utils::get_default_sources_file_path(&config_path), ToString::to_string), + ); let mappings_file = args.mapping_file.as_ref().map(|p| resolve_env_var(p)); ConfigPaths { @@ -257,7 +263,6 @@ async fn scan_library_cli(app_config: &Arc, force_rescan: bool) { std::process::exit(1); }; - match processor.scan(force_rescan).await { Ok(result) => { info!("Library scan completed successfully!"); @@ -280,27 +285,25 @@ async fn scan_library_cli(app_config: &Arc, force_rescan: bool) { async fn healthcheck(config_file: &str) -> bool { let path = std::path::PathBuf::from(config_file); match File::open(path) { - Ok(file) => { - match serde_saphyr::from_reader::<_, HealthcheckConfig>(config_file_reader(file, true)) { - Ok(config) => { - match reqwest::Client::new() - .get(format!("http://localhost:{}/healthcheck", config.api.port)) - .send() - .await - { - Ok(response) => matches!(response.json::().await, Ok(check) if check.status == "ok"), - Err(_) => false, - } - } - Err(err) => { - error!("Failed to parse config file for healthcheck {err:?}"); - false + Ok(file) => match serde_saphyr::from_reader::<_, HealthcheckConfig>(config_file_reader(file, true)) { + Ok(config) => { + match reqwest::Client::new() + .get(format!("http://localhost:{}/healthcheck", config.api.port)) + .send() + .await + { + Ok(response) => matches!(response.json::().await, Ok(check) if check.status == "ok"), + Err(_) => false, } } - } + Err(err) => { + error!("Failed to parse config file for healthcheck {err:?}"); + false + } + }, Err(err) => { error!("Failed to open config file for healthcheck {err:?}"); false } } -} \ No newline at end of file +} diff --git a/backend/src/modules.rs b/backend/src/modules.rs index 2d13e02b6..c19479738 100644 --- a/backend/src/modules.rs +++ b/backend/src/modules.rs @@ -6,14 +6,13 @@ macro_rules! include_modules { extern crate pest; pub mod api; pub mod auth; + pub mod library; pub mod messaging; pub mod model; pub mod processing; - pub mod repository; - pub mod utils; - pub mod tools; - pub mod library; pub mod ptt; - } + pub mod repository; + pub mod tools; + pub mod utils; + }; } - diff --git a/backend/src/processing/processor/sort.rs b/backend/src/processing/processor/sort.rs index 09df463aa..5640765a3 100644 --- a/backend/src/processing/processor/sort.rs +++ b/backend/src/processing/processor/sort.rs @@ -114,7 +114,7 @@ pub(in crate::processing::processor) fn sort_playlist( ) -> bool { let Some(sort) = &target.sort else { for group in &mut *playlist { - group.channels.sort_by(|a, b| a.header.source_ordinal.cmp(&b.header.source_ordinal)); + group.channels.sort_by_key(|a| a.header.source_ordinal); } sort_groups_by_source_order!(playlist); return true; @@ -206,7 +206,7 @@ fn sort_channels_in_groups( for group in groups { group .channels - .sort_by(|a, b| a.header.source_ordinal.cmp(&b.header.source_ordinal)); + .sort_by_key(|a| a.header.source_ordinal); } return; } diff --git a/backend/src/repository/xtream_repository.rs b/backend/src/repository/xtream_repository.rs index 8c0761b22..5c1e34368 100644 --- a/backend/src/repository/xtream_repository.rs +++ b/backend/src/repository/xtream_repository.rs @@ -18,7 +18,7 @@ use bytes::Bytes; use fs2::FileExt; use futures::{stream, Stream, StreamExt}; use indexmap::IndexMap; -use log::{error}; +use log::error; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::error::{info_err_res, notify_err, string_to_io_error, TuliproxError}; @@ -718,10 +718,9 @@ fn preserve_details_input_xtream_playlist_cluster_to_disk( old_path: &Path, tmp_path: &Path, ) -> Result<(), TuliproxError> { - - let Ok(mut old_tree) = BPlusTreeQuery::::try_new(old_path) else { - return Ok(()) - }; + let Ok(mut old_tree) = BPlusTreeQuery::::try_new(old_path) else { + return Ok(()) + }; let Ok(mut new_tree) = BPlusTreeUpdate::::try_new_with_backoff(tmp_path) else { return Ok(()) @@ -733,18 +732,16 @@ fn preserve_details_input_xtream_playlist_cluster_to_disk( if old_props.has_details() { if let Ok(Some(mut new_item)) = new_tree.query(&old_item.provider_id) { if let Some(new_props) = new_item.additional_properties.as_mut() { - if needs_preserved_stream_property_merge(new_props, old_props) { - if merge_preserved_stream_properties(new_props, old_props) { - updates.push((new_item.provider_id, new_item)); - if updates.len() >= BATCH_SIZE { - let refs: Vec<(&u32, &XtreamPlaylistItem)> = updates.iter().map(|(id, pli)| (id, pli)).collect(); - new_tree.update_batch(&refs).map_err(|e| notify_err!("Failed to update tmp tree during merge: {e}"))?; - updates.clear(); - } + if needs_preserved_stream_property_merge(new_props, old_props) && merge_preserved_stream_properties(new_props, old_props) { + updates.push((new_item.provider_id, new_item)); + if updates.len() >= BATCH_SIZE { + let refs: Vec<(&u32, &XtreamPlaylistItem)> = updates.iter().map(|(id, pli)| (id, pli)).collect(); + new_tree.update_batch(&refs).map_err(|e| notify_err!("Failed to update tmp tree during merge: {e}"))?; + updates.clear(); } } - }; - }; + } + } } } } @@ -1129,7 +1126,6 @@ pub(crate) fn needs_update_info_details( new_stream_props: &StreamProperties, old_stream_props: &StreamProperties, ) -> bool { - let new_modified = new_stream_props.get_last_modified(); let old_modified = old_stream_props.get_last_modified(); @@ -1148,15 +1144,15 @@ fn needs_preserved_stream_property_merge( match (new_stream_props, old_stream_props) { (StreamProperties::Video(v_new), StreamProperties::Video(v_old)) - if preserve_info_details && v_old.details.is_some() => - { - v_new.details != v_old.details - } + if preserve_info_details && v_old.details.is_some() => + { + v_new.details != v_old.details + } (StreamProperties::Series(s_new), StreamProperties::Series(s_old)) - if preserve_info_details && s_old.details.is_some() => - { - s_new.details != s_old.details - } + if preserve_info_details && s_old.details.is_some() => + { + s_new.details != s_old.details + } (StreamProperties::Live(l_new), StreamProperties::Live(l_old)) => { (l_new.video.is_none() && l_old.video.is_some()) || (l_new.audio.is_none() && l_old.audio.is_some()) @@ -1455,22 +1451,6 @@ mod tests { assert!(!needs_update_info_details(&new_props, &old_props)); } - #[test] - fn keeps_old_details_when_new_timestamp_is_newer_but_new_has_no_details() { - let new_props = StreamProperties::Video(Box::new(VideoStreamProperties { - added: "200".into(), - details: None, - ..VideoStreamProperties::default() - })); - let old_props = StreamProperties::Video(Box::new(VideoStreamProperties { - added: "100".into(), - details: Some(shared::model::VideoStreamDetailProperties::default()), - ..VideoStreamProperties::default() - })); - - assert!(!needs_update_info_details(&new_props, &old_props)); - } - #[test] fn merge_preserves_missing_live_probe_timestamps() { let mut new_props = StreamProperties::Live(Box::new(LiveStreamProperties { diff --git a/backend/src/utils/logging.rs b/backend/src/utils/logging.rs index d38dcd3a5..1b6391514 100644 --- a/backend/src/utils/logging.rs +++ b/backend/src/utils/logging.rs @@ -1,8 +1,10 @@ use crate::model::LogLevelConfig; use crate::utils::config_file_reader; +use chrono::{Local, Offset, SecondsFormat}; use env_logger::{Builder, Target}; use log::{error, info, LevelFilter}; use std::fs::File; +use std::io::Write; const LOG_ERROR_LEVEL_MOD: &[&str] = &[ "reqwest::async_impl::client", @@ -11,7 +13,6 @@ const LOG_ERROR_LEVEL_MOD: &[&str] = &[ "tungstenite::protocol" ]; - fn get_log_level(log_level: &str) -> LevelFilter { match log_level.to_lowercase().as_str() { "trace" => LevelFilter::Trace, @@ -36,6 +37,17 @@ pub fn init_logger(user_log_level: Option<&str>, config_file: &str) { let mut log_builder = Builder::from_default_env(); log_builder.target(Target::Stdout); + log_builder.format(move |buf, record| { + let now = Local::now(); + let timestamp = now.to_rfc3339_opts(SecondsFormat::Secs, now.offset().fix().local_minus_utc() == 0); + writeln!( + buf, + "[{timestamp} {} {}] {}", + record.level(), + record.target(), + record.args() + ) + }); // priority CLI-Argument, Env-Var, Config, Default let log_level = user_log_level @@ -75,5 +87,6 @@ pub fn init_logger(user_log_level: Option<&str>, config_file: &str) { log_builder.filter_module(module, LevelFilter::Error); } let _ = log_builder.try_init(); + info!("Log timezone system localtime (TZ)"); info!("Log Level {}", &log_levels.join(", ")); } diff --git a/backend/tests/proxy_redirect_auth.rs b/backend/tests/proxy_redirect_auth.rs index 24630cbc3..1a431f149 100644 --- a/backend/tests/proxy_redirect_auth.rs +++ b/backend/tests/proxy_redirect_auth.rs @@ -1,20 +1,26 @@ use base64::Engine; use bytes::Bytes; use http_body_util::Full; -use hyper::body::Incoming; -use hyper::service::service_fn; -use hyper::http::{self, HeaderName, HeaderValue}; -use hyper::{Request, Response, StatusCode}; -use hyper_util::rt::{TokioExecutor, TokioIo}; -use hyper_util::server::conn::auto::Builder; -use reqwest::redirect::Policy; -use reqwest::Proxy; -use std::convert::Infallible; -use std::net::SocketAddr; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use tokio::net::TcpListener; -use tokio::sync::Mutex; +use hyper::{ + body::Incoming, + http::{self, HeaderName, HeaderValue}, + service::service_fn, + Request, Response, StatusCode, +}; +use hyper_util::{ + rt::{TokioExecutor, TokioIo}, + server::conn::auto::Builder, +}; +use reqwest::{redirect::Policy, Proxy}; +use std::{ + convert::Infallible, + net::SocketAddr, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; +use tokio::{net::TcpListener, sync::Mutex}; use url::Url; struct ProxyState { @@ -40,15 +46,10 @@ fn response_with_status(status: StatusCode, headers: Vec<(HeaderName, HeaderValu for (name, value) in headers { builder = builder.header(name, value); } - builder - .body(Full::new(Bytes::from_static(b""))) - .unwrap() + builder.body(Full::new(Bytes::from_static(b""))).unwrap() } -async fn proxy_handler( - req: Request, - state: Arc, -) -> Result>, Infallible> { +async fn proxy_handler(req: Request, state: Arc) -> Result>, Infallible> { let request_index = state.request_count.fetch_add(1, Ordering::SeqCst) + 1; { let mut uris = state.uris.lock().await; @@ -61,20 +62,14 @@ async fn proxy_handler( state.missing_auth.fetch_add(1, Ordering::SeqCst); return Ok(response_with_status( StatusCode::PROXY_AUTHENTICATION_REQUIRED, - vec![( - http::header::PROXY_AUTHENTICATE, - HeaderValue::from_static("Basic realm=\"proxy\""), - )], + vec![(http::header::PROXY_AUTHENTICATE, HeaderValue::from_static("Basic realm=\"proxy\""))], )); } if request_index == 1 { return Ok(response_with_status( StatusCode::FOUND, - vec![( - http::header::LOCATION, - HeaderValue::from_static("http://redirect.local/ok"), - )], + vec![(http::header::LOCATION, HeaderValue::from_static("http://redirect.local/ok"))], )); } @@ -102,11 +97,7 @@ async fn start_proxy(state: Arc) -> (SocketAddr, tokio::task::JoinHa (addr, handle) } -async fn send_with_manual_redirects( - client: &reqwest::Client, - mut url: Url, - max_redirects: u8, -) -> reqwest::Response { +async fn send_with_manual_redirects(client: &reqwest::Client, mut url: Url, max_redirects: u8) -> reqwest::Response { let mut remaining = max_redirects; loop { let response = client.get(url.clone()).send().await.unwrap(); @@ -122,9 +113,7 @@ async fn send_with_manual_redirects( let Ok(location_str) = location.to_str() else { return response; }; - let next_url = url - .join(location_str) - .or_else(|_| Url::parse(location_str)); + let next_url = url.join(location_str).or_else(|_| Url::parse(location_str)); let Ok(next_url) = next_url else { return response; }; @@ -139,8 +128,7 @@ async fn send_with_manual_redirects( async fn run_proxy_flow(manual_redirects: bool) -> (StatusCode, usize, Vec) { let credentials = "user:pass"; let encoded = base64::engine::general_purpose::STANDARD.encode(credentials); - let expected_auth = - HeaderValue::from_str(&format!("Basic {encoded}")).unwrap(); + let expected_auth = HeaderValue::from_str(&format!("Basic {encoded}")).unwrap(); let state = Arc::new(ProxyState::new(expected_auth.clone())); let (addr, server_handle) = start_proxy(Arc::clone(&state)).await; @@ -148,27 +136,14 @@ async fn run_proxy_flow(manual_redirects: bool) -> (StatusCode, usize, Vec" - echo - echo "Arguments:" - echo " branch Git branch name (only 'master' and 'develop' are supported)" - echo - echo "Examples:" - echo " $(basename "$0") master # Builds and pushes with :VERSION and :latest tags" - echo " $(basename "$0") develop # Builds and pushes with :VERSION and :dev tags" - exit 1 -} - # Validate arguments if [ $# -ne 1 ]; then - echo "🧨 Error: Exactly one argument required" - print_usage + echo "Usage: $(basename "$0") " + exit 1 fi BRANCH="$1" - -# Validate branch case "$BRANCH" in - master) - TAG_SUFFIX="latest" - ;; - develop) - TAG_SUFFIX="dev" - ;; - *) - echo "🧨 Error: Branch '$BRANCH' is not supported. Only 'master' and 'develop' are allowed." - exit 1 - ;; + master) TAG_SUFFIX="latest" ;; + develop) TAG_SUFFIX="dev" ;; + *) echo "🧨 Error: Branch '$BRANCH' not supported"; exit 1 ;; esac -echo "πŸš€ Building Docker images for branch: $BRANCH (tag: $TAG_SUFFIX)" +echo "πŸš€ Building for branch: $BRANCH (tag: $TAG_SUFFIX)" -# Set up directories +# Directories WORKING_DIR=$(pwd) -BIN_DIR="${WORKING_DIR}/bin" -RESOURCES_DIR="${WORKING_DIR}/resources" DOCKER_DIR="${WORKING_DIR}/docker" BACKEND_DIR="${WORKING_DIR}/backend" FRONTEND_DIR="${WORKING_DIR}/frontend" FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist" -# Define architectures and their corresponding builds -declare -A ARCHITECTURES=( - [LINUX]=x86_64-unknown-linux-musl - [AARCH64]=aarch64-unknown-linux-musl -) +# Config +declare -A ARCHITECTURES=([LINUX]=x86_64-unknown-linux-musl [AARCH64]=aarch64-unknown-linux-musl) +declare -A MULTI_PLATFORM_IMAGES=([tuliprox]="scratch-final" [tuliprox-alpine]="alpine-final") -# Images to build with multi-platform support -declare -A MULTI_PLATFORM_IMAGES=( - [tuliprox]="scratch-final" - [tuliprox-alpine]="alpine-final" -) - -# Get version from Cargo.toml +# Version detection VERSION=$(grep -Po '^version\s*=\s*"\K[0-9\.]+' "${BACKEND_DIR}/Cargo.toml") -if [ -z "${VERSION}" ]; then - echo "🧨 Error: Failed to determine the version from Cargo.toml" - exit 1 -fi - echo "πŸ“¦ Version: ${VERSION}" -# Build resources if needed (check if resources are already built) -# Note: Docker build handles resource creation with its own ffmpeg container -RESOURCES_BUILT=true -for resource in "channel_unavailable.ts" "user_connections_exhausted.ts" "provider_connections_exhausted.ts" "user_account_expired.ts" "panel_api_provisioning.ts"; do - if [ ! -f "${RESOURCES_DIR}/${resource}" ]; then - RESOURCES_BUILT=false - break - fi -done - -if [ "$RESOURCES_BUILT" = "false" ] && [ -f "${BIN_DIR}/build_resources.sh" ]; then - echo "πŸ› οΈ Building resources..." - if ! "${BIN_DIR}/build_resources.sh"; then - echo "⚠️ Resource building failed, but Docker build will handle resource creation" - fi -elif [ "$RESOURCES_BUILT" = "true" ]; then - echo "πŸ› οΈ Resources already built, skipping..." -fi - -# Build frontend (skip if cached) -if [ "${FRONTEND_CACHE_HIT:-false}" = "true" ] && [ -d "${FRONTEND_BUILD_DIR}" ]; then - echo "🎨 Frontend build found in cache, skipping build..." +# ----------------------------------------- +# 1. Frontend Build +# ----------------------------------------- +if [ -d "${FRONTEND_BUILD_DIR}" ]; then + echo "🎨 Frontend build found, skipping build..." else echo "🎨 Building frontend..." - rm -rf "${FRONTEND_BUILD_DIR}" - cd "${FRONTEND_DIR}" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release - - # Check if the frontend build directory exists - if [ ! -d "${FRONTEND_BUILD_DIR}" ]; then - echo "🧨 Error: Frontend build directory '${FRONTEND_BUILD_DIR}' does not exist" - exit 1 - fi + cd "${FRONTEND_DIR}" && trunk build --release fi cd "$WORKING_DIR" -# Build binaries for all architectures first -echo "πŸ—οΈ Building binaries for all architectures..." - -# Avoid executing stale build artifacts when caches are missing or the runner image changes. -if [ "${TULIPROX_FORCE_CLEAN:-0}" = "1" ] || [ "${CARGO_DEPS_CACHE_HIT:-false}" != "true" ]; then - cargo clean || true -fi +# ----------------------------------------- +# 2. Binary Compilation (Multi-Arch) +# ----------------------------------------- +echo "πŸ—οΈ Building binaries..." +mkdir -p "${DOCKER_DIR}/binaries" for PLATFORM in "${!ARCHITECTURES[@]}"; do ARCHITECTURE=${ARCHITECTURES[$PLATFORM]} - - echo "πŸ”¨ Building binary for architecture: $ARCHITECTURE" + echo "πŸ”¨ Building for $ARCHITECTURE" - # Use incremental compilation and enable cache-friendly flags - env RUSTFLAGS="--remap-path-prefix $HOME=~ -C incremental=/tmp/rust-incremental-${ARCHITECTURE}" \ - CARGO_INCREMENTAL=1 \ - cross build -p tuliprox --release --target "$ARCHITECTURE" --locked - - BINARY_PATH="${WORKING_DIR}/target/${ARCHITECTURE}/release/tuliprox" - if [ ! -f "$BINARY_PATH" ]; then - echo "🧨 Error: Binary $BINARY_PATH does not exist" - exit 1 - fi - - # Copy binary with architecture suffix for multi-platform build - mkdir -p "${DOCKER_DIR}/binaries" - cp "$BINARY_PATH" "${DOCKER_DIR}/binaries/tuliprox-${ARCHITECTURE}" + # Using cross for compilation + cross build -p tuliprox --release --target "$ARCHITECTURE" --locked + + cp "target/${ARCHITECTURE}/release/tuliprox" "${DOCKER_DIR}/binaries/tuliprox-${ARCHITECTURE}" done -# Prepare common Docker context +# ----------------------------------------- +# 3. Docker Build & Push (Optimized Caching) +# ----------------------------------------- echo "πŸ“‹ Preparing Docker context..." -rm -rf "${DOCKER_DIR}/web" -rm -rf "${DOCKER_DIR}/resources" cp -r "${FRONTEND_BUILD_DIR}" "${DOCKER_DIR}/web" -cp -r "${RESOURCES_DIR}" "${DOCKER_DIR}/resources" +cp -r "resources" "${DOCKER_DIR}/resources" cd "${DOCKER_DIR}" -# Login to GitHub Container Registry (needed before buildx push) -echo "πŸ”‘ Logging into GitHub Container Registry..." -printf '%s' "${GITHUB_IO_TOKEN}" | docker login ghcr.io --username "${REPO_OWNER}" --password-stdin +# Login +echo "πŸ”‘ Logging into GHCR..." +echo "${GITHUB_IO_TOKEN}" | docker login ghcr.io -u "${REPO_OWNER}" --password-stdin -declare -a BUILT_IMAGES=() +REPO_OWNER_LC="${REPO_OWNER,,}" -# Build multi-platform images for IMAGE_NAME in "${!MULTI_PLATFORM_IMAGES[@]}"; do BUILD_TARGET="${MULTI_PLATFORM_IMAGES[$IMAGE_NAME]}" - - echo "🎯 Building multi-platform image: ${IMAGE_NAME} with target ${BUILD_TARGET}" - - # Prepare tags based on branch - REPO_OWNER_LC="${REPO_OWNER,,}" - DOCKER_TAGS="-t ghcr.io/${REPO_OWNER_LC}/${IMAGE_NAME}:${VERSION} -t ghcr.io/${REPO_OWNER_LC}/${IMAGE_NAME}:${TAG_SUFFIX}" - BUILT_IMAGES+=("ghcr.io/${REPO_OWNER_LC}/${IMAGE_NAME}:${VERSION}") - BUILT_IMAGES+=("ghcr.io/${REPO_OWNER_LC}/${IMAGE_NAME}:${TAG_SUFFIX}") + TAG_VERSION="ghcr.io/${REPO_OWNER_LC}/${IMAGE_NAME}:${VERSION}" + TAG_BRANCH="ghcr.io/${REPO_OWNER_LC}/${IMAGE_NAME}:${TAG_SUFFIX}" - # Build and push multi-platform image directly with cache - BUILDX_CACHE_ARGS=() - [ -n "${BUILDX_CACHE_FROM:-}" ] && BUILDX_CACHE_ARGS+=(--cache-from "${BUILDX_CACHE_FROM}") - [ -n "${BUILDX_CACHE_TO:-}" ] && BUILDX_CACHE_ARGS+=(--cache-to "${BUILDX_CACHE_TO}") - - # If you build on local, you need to activate buildx multiarch builds - # > docker buildx create --name multiarch --driver docker-container --use --bootstrap - # > docker run --rm --privileged tonistiigi/binfmt:latest --install all + echo "🎯 Building multi-platform image: ${IMAGE_NAME}" + # THE FIX: Using type=gha for automatic GitHub Actions cache management. + # No more local files, no more leftover artifacts. docker buildx build -f Dockerfile-manual \ - ${DOCKER_TAGS} \ + -t "${TAG_VERSION}" \ + -t "${TAG_BRANCH}" \ --target "$BUILD_TARGET" \ --platform "linux/amd64,linux/arm64" \ - "${BUILDX_CACHE_ARGS[@]}" \ + --cache-from "type=gha,scope=${IMAGE_NAME}" \ + --cache-to "type=gha,mode=max,scope=${IMAGE_NAME}" \ --push \ . done -# Clean up Docker context -echo "πŸ—‘οΈ Cleaning up binaries..." -rm -rf "${DOCKER_DIR}/binaries" - -cd "$WORKING_DIR" - -# Final cleanup -echo "πŸ—‘οΈ Cleaning up Docker context..." -rm -rf "${DOCKER_DIR}/web" -rm -f "${DOCKER_DIR}/tuliprox" -rm -rf "${DOCKER_DIR}/resources" - -echo "πŸŽ‰ Docker images for branch '$BRANCH' (version ${VERSION}) have been successfully built and pushed!" -echo "πŸ“‹ Built images:" -for img in "${BUILT_IMAGES[@]}"; do - echo " - $img" -done +# ----------------------------------------- +# Cleanup +# ----------------------------------------- +echo "πŸ—‘οΈ Final cleanup..." +rm -rf "${DOCKER_DIR}/binaries" "${DOCKER_DIR}/web" "${DOCKER_DIR}/resources" diff --git a/frontend/src/app/components/accordion.rs b/frontend/src/app/components/accordion.rs index cb13ce856..d6648e2b1 100644 --- a/frontend/src/app/components/accordion.rs +++ b/frontend/src/app/components/accordion.rs @@ -1,5 +1,5 @@ -use yew::prelude::*; use std::rc::Rc; +use yew::prelude::*; #[derive(Debug, Clone, PartialEq)] pub struct AccordionState { @@ -17,9 +17,7 @@ pub struct AccordionProps { #[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), - }; + let state = AccordionState { active_panel: Rc::new(active_panel) }; html! { context={state}> diff --git a/frontend/src/app/components/accordion_panel.rs b/frontend/src/app/components/accordion_panel.rs index fb7581ff3..418c01aef 100644 --- a/frontend/src/app/components/accordion_panel.rs +++ b/frontend/src/app/components/accordion_panel.rs @@ -1,5 +1,5 @@ -use yew::prelude::*; use crate::app::components::{AccordionState, AppIcon}; +use yew::prelude::*; #[derive(Properties, Clone, PartialEq, Debug)] pub struct AccordionPanelProps { @@ -45,4 +45,4 @@ pub fn AccordionPanel(props: &AccordionPanelProps) -> Html { } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/api_user/api_user_view.rs b/frontend/src/app/components/api_user/api_user_view.rs index 3a798a7a0..f79e561cb 100644 --- a/frontend/src/app/components/api_user/api_user_view.rs +++ b/frontend/src/app/components/api_user/api_user_view.rs @@ -1,11 +1,12 @@ -use yew::{function_component, html, Callback, Html}; -use crate::app::components::loading_indicator::BusyIndicator; -use crate::app::components::{AppIcon, IconButton, ToastrView, WebsocketStatus}; -use crate::app::components::theme::Theme; -use crate::hooks::use_service_context; -use crate::provider::DialogProvider; -use yew::use_state; -use crate::app::components::api_user::playlist::ApiUserPlaylist; +use crate::{ + app::components::{ + api_user::playlist::ApiUserPlaylist, loading_indicator::BusyIndicator, theme::Theme, AppIcon, IconButton, + ToastrView, WebsocketStatus, + }, + hooks::use_service_context, + provider::DialogProvider, +}; +use yew::{function_component, html, use_state, Callback, Html}; #[function_component] pub fn ApiUserView() -> Html { @@ -56,4 +57,4 @@ pub fn ApiUserView() -> Html { } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/api_user/mod.rs b/frontend/src/app/components/api_user/mod.rs index 6553e0328..b5568ece0 100644 --- a/frontend/src/app/components/api_user/mod.rs +++ b/frontend/src/app/components/api_user/mod.rs @@ -2,4 +2,4 @@ mod api_user_view; mod playlist; mod target_playlist; -pub use api_user_view::*; \ No newline at end of file +pub use api_user_view::*; diff --git a/frontend/src/app/components/api_user/playlist.rs b/frontend/src/app/components/api_user/playlist.rs index bc1407213..4162fcb6b 100644 --- a/frontend/src/app/components/api_user/playlist.rs +++ b/frontend/src/app/components/api_user/playlist.rs @@ -1,14 +1,16 @@ -use crate::app::components::api_user::target_playlist::{BouquetSelection, UserTargetPlaylist}; -use crate::app::components::{Panel, RadioButtonGroup, TextButton}; -use crate::hooks::use_service_context; -use crate::model::{BusyStatus, EventMessage}; -use shared::error::{TuliproxError, info_err_res}; -use shared::model::{PlaylistBouquetDto, PlaylistCategoriesDto, PlaylistClusterBouquetDto}; -use std::cell::RefCell; -use std::collections::HashMap; -use std::fmt; -use std::rc::Rc; -use std::str::FromStr; +use crate::{ + app::components::{ + api_user::target_playlist::{BouquetSelection, UserTargetPlaylist}, + Panel, RadioButtonGroup, TextButton, + }, + hooks::use_service_context, + model::{BusyStatus, EventMessage}, +}; +use shared::{ + error::{info_err_res, TuliproxError}, + model::{PlaylistBouquetDto, PlaylistCategoriesDto, PlaylistClusterBouquetDto}, +}; +use std::{cell::RefCell, collections::HashMap, fmt, rc::Rc, str::FromStr}; use yew::prelude::*; use yew_i18n::use_translation; @@ -40,16 +42,20 @@ impl fmt::Display for ApiUserPlaylistPage { } } -fn to_playlist_cluster(count: (usize, usize, usize), bouquet: Option<&Rc>>) -> Option { +fn to_playlist_cluster( + count: (usize, usize, usize), + bouquet: Option<&Rc>>, +) -> Option { if let Some(bouq) = bouquet { let selections = bouq.borrow(); let selected_vec = |map: &HashMap| { - let v: Vec = map.iter() - .filter(|(_, &selected)| selected) - .map(|(c, _)| c.clone()) - .collect(); - if v.is_empty() { None } else { Some(v) } + let v: Vec = map.iter().filter(|(_, &selected)| selected).map(|(c, _)| c.clone()).collect(); + if v.is_empty() { + None + } else { + Some(v) + } }; let live = selected_vec(&selections.live).filter(|v| v.len() != count.0); @@ -79,9 +85,7 @@ pub fn ApiUserPlaylist() -> Html { }); // Selection reference - let selections = use_mut_ref(|| { - HashMap::>>::new() - }); + let selections = use_mut_ref(HashMap::>>::new); let handle_tab_select = { let active_tab_clone = active_tab.clone(); @@ -104,7 +108,8 @@ pub fn ApiUserPlaylist() -> Html { use_effect_with((), move |_| { wasm_bindgen_futures::spawn_local(async move { services.event.broadcast(EventMessage::Busy(BusyStatus::Show)); - let result = (services.user_api.get_playlist_bouquet().await, services.user_api.get_playlist_categories().await); + let result = + (services.user_api.get_playlist_bouquet().await, services.user_api.get_playlist_categories().await); match result { (Ok(bouquet), Ok(cats)) => { bouquets.set(bouquet.clone()); @@ -137,16 +142,30 @@ pub fn ApiUserPlaylist() -> Html { let selections = selections.clone(); let services = services.clone(); let translate = translate.clone(); - let categories_xtream_count = categories.as_ref().and_then(|plc| plc.xtream.as_ref().map(|x| - (x.live.as_ref().map(|v| v.len()).unwrap_or(0), - x.vod.as_ref().map(|v| v.len()).unwrap_or(0), - x.series.as_ref().map(|v| v.len()).unwrap_or(0)) - )).unwrap_or((0, 0, 0)); - let categories_m3u_count = categories.as_ref().and_then(|plc| plc.m3u.as_ref().map(|x| - (x.live.as_ref().map(|v| v.len()).unwrap_or(0), - x.vod.as_ref().map(|v| v.len()).unwrap_or(0), - x.series.as_ref().map(|v| v.len()).unwrap_or(0)) - )).unwrap_or((0, 0, 0)); + let categories_xtream_count = categories + .as_ref() + .and_then(|plc| { + plc.xtream.as_ref().map(|x| { + ( + x.live.as_ref().map(|v| v.len()).unwrap_or(0), + x.vod.as_ref().map(|v| v.len()).unwrap_or(0), + x.series.as_ref().map(|v| v.len()).unwrap_or(0), + ) + }) + }) + .unwrap_or((0, 0, 0)); + let categories_m3u_count = categories + .as_ref() + .and_then(|plc| { + plc.m3u.as_ref().map(|x| { + ( + x.live.as_ref().map(|v| v.len()).unwrap_or(0), + x.vod.as_ref().map(|v| v.len()).unwrap_or(0), + x.series.as_ref().map(|v| v.len()).unwrap_or(0), + ) + }) + }) + .unwrap_or((0, 0, 0)); wasm_bindgen_futures::spawn_local(async move { services.event.broadcast(EventMessage::Busy(BusyStatus::Show)); @@ -159,7 +178,7 @@ pub fn ApiUserPlaylist() -> Html { }; match services.user_api.save_playlist_bouquet(&result).await { - Ok(()) => services.toastr.success(translate.t("MESSAGES.SAVE.BOUQUET.SUCCESS")), + Ok(()) => services.toastr.success(translate.t("MESSAGES.SAVE.BOUQUET.SUCCESS")), Err(_) => services.toastr.error(translate.t("MESSAGES.SAVE.BOUQUET.FAIL")), } diff --git a/frontend/src/app/components/api_user/target_playlist.rs b/frontend/src/app/components/api_user/target_playlist.rs index 07567357d..c372c60ed 100644 --- a/frontend/src/app/components/api_user/target_playlist.rs +++ b/frontend/src/app/components/api_user/target_playlist.rs @@ -1,22 +1,25 @@ -use std::cell::RefCell; -use crate::app::components::{AppIcon, Card, CollapsePanel, IconButton, RadioButtonGroup}; -use shared::model::{PlaylistClusterBouquetDto, PlaylistClusterCategoriesDto, XtreamCluster}; -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::rc::Rc; -use std::str::FromStr; +use crate::{ + app::components::{AppIcon, Card, CollapsePanel, IconButton, RadioButtonGroup}, + html_if, +}; +use shared::{ + error::TuliproxError, + info_err_res, + model::{PlaylistClusterBouquetDto, PlaylistClusterCategoriesDto, XtreamCluster}, +}; +use std::{ + cell::RefCell, + collections::HashMap, + fmt::{Display, Formatter}, + rc::Rc, + str::FromStr, +}; use wasm_bindgen::JsCast; use yew::prelude::*; use yew_i18n::use_translation; -use shared::error::TuliproxError; -use shared::info_err_res; -use crate::html_if; fn normalize(s: &str) -> String { - let cleaned: String = s - .chars() - .filter(|c| c.is_alphanumeric() || c.is_whitespace()) - .collect(); + let cleaned: String = s.chars().filter(|c| c.is_alphanumeric() || c.is_whitespace()).collect(); cleaned.trim().to_lowercase() } @@ -57,12 +60,14 @@ impl FilterState { impl Display for FilterState { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", - match self { - Self::All => Self::ALL, - Self::Selected => Self::SELECTED, - Self::Deselected => Self::DESELECTED, - } + write!( + f, + "{}", + match self { + Self::All => Self::ALL, + Self::Selected => Self::SELECTED, + Self::Deselected => Self::DESELECTED, + } ) } } @@ -203,7 +208,9 @@ pub fn UserTargetPlaylist(props: &UserTargetPlaylistProps) -> Html { }) }; - let render_category_cluster = |cluster: XtreamCluster, cats: Option<&Vec>, selections: &HashMap| { + let render_category_cluster = |cluster: XtreamCluster, + cats: Option<&Vec>, + selections: &HashMap| { if let Some(c) = cats { let cats_clone = c.clone(); let cluster_clone = cluster; @@ -229,7 +236,8 @@ pub fn UserTargetPlaylist(props: &UserTargetPlaylistProps) -> Html { }; let filter_state_handle = filter_state.clone(); - let filter_state_selections = Rc::new(vec![filter_state.get(&cluster).cloned().unwrap_or(FilterState::All).to_string()]); + let filter_state_selections = + Rc::new(vec![filter_state.get(&cluster).cloned().unwrap_or(FilterState::All).to_string()]); let title_content = if *collapse_state.get(&cluster).unwrap_or(&true) { html! {
diff --git a/frontend/src/app/components/authentication.rs b/frontend/src/app/components/authentication.rs index 63bd4ac4b..5f5d58848 100644 --- a/frontend/src/app/components/authentication.rs +++ b/frontend/src/app/components/authentication.rs @@ -1,8 +1,6 @@ -use crate::app::components::login::Login; -use crate::hooks::use_service_context; +use crate::{app::components::login::Login, hooks::use_service_context}; use std::future; -use yew::prelude::*; -use yew::suspense::use_future; +use yew::{prelude::*, suspense::use_future}; use yew_hooks::{use_async_with_options, UseAsyncOptions}; #[derive(Properties, Clone, PartialEq)] @@ -20,15 +18,16 @@ pub fn Authentication(props: &AuthenticationProps) -> Html { let services_ctx = services.clone(); let authenticated_state = authenticated.clone(); let _ = use_future(|| async move { - services_ctx.auth.auth_subscribe( - &mut |success| { + services_ctx + .auth + .auth_subscribe(&mut |success| { authenticated_state.set(success); if success { services_ctx.websocket.connect_ws_with_backoff(); } future::ready(()) - } - ).await + }) + .await }); } @@ -36,13 +35,16 @@ pub fn Authentication(props: &AuthenticationProps) -> Html { let services_ctx = services.clone(); let authenticated_state = authenticated.clone(); let loading_state = loading.clone(); - use_async_with_options(async move { - let result = services_ctx.auth.refresh().await; - let success = result.is_ok(); - authenticated_state.set(success); - loading_state.set(false); - result - }, UseAsyncOptions::enable_auto()); + use_async_with_options( + async move { + let result = services_ctx.auth.refresh().await; + let success = result.is_ok(); + authenticated_state.set(success); + loading_state.set(false); + result + }, + UseAsyncOptions::enable_auto(), + ); } if *loading { @@ -54,4 +56,4 @@ pub fn Authentication(props: &AuthenticationProps) -> Html { } else { html! {} } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/breadcrumbs.rs b/frontend/src/app/components/breadcrumbs.rs index 83d11e1be..810884b3b 100644 --- a/frontend/src/app/components/breadcrumbs.rs +++ b/frontend/src/app/components/breadcrumbs.rs @@ -1,8 +1,7 @@ -use std::rc::Rc; -use std::str::FromStr; +use crate::app::components::AppIcon; +use std::{rc::Rc, str::FromStr}; use wasm_bindgen::JsCast; use yew::prelude::*; -use crate::app::components::AppIcon; #[derive(Properties, PartialEq, Clone)] pub struct BreadcrumbsProps { diff --git a/frontend/src/app/components/card.rs b/frontend/src/app/components/card.rs index ec4a85ab5..c08107fc4 100644 --- a/frontend/src/app/components/card.rs +++ b/frontend/src/app/components/card.rs @@ -1,5 +1,5 @@ -use yew::prelude::*; use crate::app::CardContext; +use yew::prelude::*; #[derive(Properties, Clone, PartialEq, Debug)] pub struct CardProps { @@ -11,9 +11,7 @@ pub struct CardProps { #[function_component] pub fn Card(props: &CardProps) -> Html { let custom_class = use_state(String::new); - let context = CardContext { - custom_class: custom_class.clone(), - }; + let context = CardContext { custom_class: custom_class.clone() }; html! { context={context}>
@@ -21,4 +19,4 @@ pub fn Card(props: &CardProps) -> Html {
> } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/cell_value.rs b/frontend/src/app/components/cell_value.rs index f6a55f533..0d54f920b 100644 --- a/frontend/src/app/components/cell_value.rs +++ b/frontend/src/app/components/cell_value.rs @@ -6,13 +6,11 @@ pub enum CellValue<'a> { Status(ProxyUserStatus), Proxy(ProxyType), Text(&'a str), - Date(i64) + Date(i64), } impl<'a> PartialOrd for CellValue<'a> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl<'a> Ord for CellValue<'a> { @@ -30,7 +28,9 @@ impl<'a> Ord for CellValue<'a> { (CellValue::Bool(_), _) => std::cmp::Ordering::Less, (CellValue::Status(_), CellValue::Empty | CellValue::Bool(_)) => std::cmp::Ordering::Greater, (CellValue::Status(_), _) => std::cmp::Ordering::Less, - (CellValue::Proxy(_), CellValue::Empty | CellValue::Bool(_) | CellValue::Status(_)) => std::cmp::Ordering::Greater, + (CellValue::Proxy(_), CellValue::Empty | CellValue::Bool(_) | CellValue::Status(_)) => { + std::cmp::Ordering::Greater + } (CellValue::Proxy(_), _) => std::cmp::Ordering::Less, (CellValue::Text(_), CellValue::Date(_)) => std::cmp::Ordering::Less, (CellValue::Text(_), _) => std::cmp::Ordering::Greater, @@ -40,9 +40,7 @@ impl<'a> Ord for CellValue<'a> { } impl<'a> PartialEq for CellValue<'a> { - fn eq(&self, other: &Self) -> bool { - self.cmp(other) == std::cmp::Ordering::Equal - } + fn eq(&self, other: &Self) -> bool { self.cmp(other) == std::cmp::Ordering::Equal } } -impl<'a> Eq for CellValue<'a> {} \ No newline at end of file +impl<'a> Eq for CellValue<'a> {} diff --git a/frontend/src/app/components/chip.rs b/frontend/src/app/components/chip.rs index 66161af5f..2c16b0a47 100644 --- a/frontend/src/app/components/chip.rs +++ b/frontend/src/app/components/chip.rs @@ -1,5 +1,5 @@ -use yew::{classes, function_component, html, Callback, Html, Properties}; use crate::app::components::AppIcon; +use yew::{classes, function_component, html, Callback, Html, Properties}; pub fn convert_bool_to_chip_style(value: bool) -> Option { Option::from((if value { "active" } else { "inactive" }).to_string()) @@ -18,7 +18,6 @@ pub struct ChipProps { #[function_component] pub fn Chip(props: &ChipProps) -> Html { - let handle_remove = { if props.removable { let on_remove = props.on_remove.clone(); @@ -32,7 +31,7 @@ pub fn Chip(props: &ChipProps) -> Html { let remove = handle_remove.clone(); let label = props.label.clone(); let on_remove = Callback::from(move |_| remove.emit(label.clone())); - html ! { + html! { diff --git a/frontend/src/app/components/cluster_flags_input.rs b/frontend/src/app/components/cluster_flags_input.rs index 455370a11..7b02778f6 100644 --- a/frontend/src/app/components/cluster_flags_input.rs +++ b/frontend/src/app/components/cluster_flags_input.rs @@ -1,4 +1,4 @@ -use shared::model::{ClusterFlags}; +use shared::model::ClusterFlags; use yew::prelude::*; use yew_i18n::use_translation; @@ -17,7 +17,7 @@ pub struct ClusterFlagsInputProps { #[prop_or_default] pub on_change: Callback<(String, Option)>, #[prop_or_default] - pub mode: ClusterFlagsInputMode, + pub mode: ClusterFlagsInputMode, } #[function_component] @@ -25,7 +25,7 @@ pub fn ClusterFlagsInput(props: &ClusterFlagsInputProps) -> Html { let translate = use_translation(); let flags = use_state(|| { - props.value.unwrap_or_else(|| match props.mode { + props.value.unwrap_or_else(|| match props.mode { ClusterFlagsInputMode::NoneIsAll => ClusterFlags::all(), ClusterFlagsInputMode::NoneIsNone => ClusterFlags::empty(), }) @@ -33,25 +33,21 @@ pub fn ClusterFlagsInput(props: &ClusterFlagsInputProps) -> Html { { let set_flags = flags.clone(); use_effect_with((props.value, props.mode), move |(val, cmode)| { - set_flags.set((*val).unwrap_or_else(|| match cmode { - ClusterFlagsInputMode::NoneIsAll => ClusterFlags::all(), - ClusterFlagsInputMode::NoneIsNone => ClusterFlags::empty(), - })); + set_flags.set((*val).unwrap_or_else(|| match cmode { + ClusterFlagsInputMode::NoneIsAll => ClusterFlags::all(), + ClusterFlagsInputMode::NoneIsNone => ClusterFlags::empty(), + })); }); } let handle_change = { - let onchange = props.on_change.clone(); - let name = props.name.clone(); - Callback::from(move |new_flags: Option| { - let cluster_flags = if new_flags.is_none_or(|f| f.is_empty()) { - None - } else { - new_flags - }; - let name = name.clone(); - onchange.emit((name, cluster_flags)); - }) + let onchange = props.on_change.clone(); + let name = props.name.clone(); + Callback::from(move |new_flags: Option| { + let cluster_flags = if new_flags.is_none_or(|f| f.is_empty()) { None } else { new_flags }; + let name = name.clone(); + onchange.emit((name, cluster_flags)); + }) }; let handle_flag_click = { @@ -84,4 +80,4 @@ pub fn ClusterFlagsInput(props: &ClusterFlagsInputProps) -> Html { { translate.t("LABEL.SERIES") }
} -} \ No newline at end of file +} diff --git a/frontend/src/app/components/collapse_panel.rs b/frontend/src/app/components/collapse_panel.rs index ee5abc006..19c1822fb 100644 --- a/frontend/src/app/components/collapse_panel.rs +++ b/frontend/src/app/components/collapse_panel.rs @@ -1,5 +1,5 @@ -use yew::prelude::*; use crate::app::components::AppIcon; +use yew::prelude::*; #[derive(Properties, Clone, PartialEq, Debug)] pub struct CollapsePanelProps { @@ -44,4 +44,4 @@ pub fn CollapsePanel(props: &CollapsePanelProps) -> Html { } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/config/api_config_view.rs b/frontend/src/app/components/config/api_config_view.rs index 028489631..48737d609 100644 --- a/frontend/src/app/components/config/api_config_view.rs +++ b/frontend/src/app/components/config/api_config_view.rs @@ -1,19 +1,28 @@ -use std::fmt::Display; -use crate::app::components::config::config_page::{ConfigForm, LABEL_API_CONFIG}; -use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::{AppIcon, Card, NoContent, Table, TableDefinition, TextButton}; -use crate::app::context::ConfigContext; -use crate::{config_field, config_field_bool, config_field_empty, edit_field_bool, edit_field_number_u16, - edit_field_text, generate_form_reducer, html_if}; -use shared::model::{ApiProxyConfigDto, ApiProxyServerInfoDto, ConfigApiDto, SortOrder}; -use std::rc::Rc; -use std::str::FromStr; +use crate::{ + app::{ + components::{ + config::{ + config_page::{ConfigForm, LABEL_API_CONFIG}, + config_view_context::ConfigViewContext, + }, + menu_item::MenuItem, + popup_menu::PopupMenu, + AppIcon, Card, NoContent, Table, TableDefinition, TextButton, + }, + context::ConfigContext, + }, + config_field, config_field_bool, config_field_empty, edit_field_bool, edit_field_number_u16, edit_field_text, + generate_form_reducer, html_if, +}; +use shared::{ + concat_string, + error::TuliproxError, + info_err_res, + model::{ApiProxyConfigDto, ApiProxyServerInfoDto, ConfigApiDto, SortOrder}, +}; +use std::{fmt::Display, rc::Rc, str::FromStr}; use yew::prelude::*; use yew_i18n::use_translation; -use shared::{concat_string, info_err_res}; -use shared::error::TuliproxError; -use crate::app::components::menu_item::MenuItem; -use crate::app::components::popup_menu::PopupMenu; const LABEL_HOST: &str = "LABEL.HOST"; const LABEL_PORT: &str = "LABEL.PORT"; @@ -28,17 +37,7 @@ const LABEL_USE_USER_DB: &str = "LABEL.USE_USER_DB"; const LABEL_SERVER: &str = "LABEL.SERVER"; const LABEL_ADD_SERVER: &str = "LABEL.ADD_SERVER"; -const SERVER_HEADERS: [&str; 8] = [ - "EMPTY", - "NAME", - "PROTOCOL", - "HOST", - "PORT", - "TIMEZONE", - "MESSAGE", - "PATH", -]; - +const SERVER_HEADERS: [&str; 8] = ["EMPTY", "NAME", "PROTOCOL", "HOST", "PORT", "TIMEZONE", "MESSAGE", "PATH"]; #[derive(Debug, Clone, Eq, PartialEq)] enum ServerTableAction { @@ -48,10 +47,14 @@ enum ServerTableAction { impl Display for ServerTableAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { - Self::Delete => "Delete", - Self::Edit => "Edit", - }) + write!( + f, + "{}", + match self { + Self::Delete => "Delete", + Self::Edit => "Edit", + } + ) } } @@ -89,7 +92,6 @@ generate_form_reducer!( } ); - #[function_component] pub fn ApiConfigView() -> Html { let translate = use_translation(); @@ -99,13 +101,11 @@ pub fn ApiConfigView() -> Html { let popup_is_open = use_state(|| false); let selected_dto = use_state(|| None::>); - let form_state_api_config: UseReducerHandle = use_reducer(|| { - ApiConfigFormState { form: ConfigApiDto::default(), modified: false } - }); + let form_state_api_config: UseReducerHandle = + use_reducer(|| ApiConfigFormState { form: ConfigApiDto::default(), modified: false }); - let form_state_api_proxy_config: UseReducerHandle = use_reducer(|| { - ApiProxyConfigFormState { form: ApiProxyConfigDto::default(), modified: false } - }); + let form_state_api_proxy_config: UseReducerHandle = + use_reducer(|| ApiProxyConfigFormState { form: ApiProxyConfigDto::default(), modified: false }); { let on_form_change = config_view_ctx.on_form_change.clone(); @@ -127,10 +127,7 @@ pub fn ApiConfigView() -> Html { { let form_state_api_config = form_state_api_config.clone(); - let api_config = config_ctx - .config - .as_ref() - .map(|c| c.config.api.clone()); + let api_config = config_ctx.config.as_ref().map(|c| c.config.api.clone()); let deps = (api_config, *config_view_ctx.edit_mode); use_effect_with(deps, move |(cfg, _mode)| { @@ -181,7 +178,6 @@ pub fn ApiConfigView() -> Html { }) }; - let handle_popup_close = { let set_is_open = popup_is_open.clone(); Callback::from(move |()| { @@ -228,30 +224,29 @@ pub fn ApiConfigView() -> Html { let popup_onclick = handle_popup_onclick.clone(); let edit_mode_ref = edit_mode_ref.clone(); Callback::<(usize, usize, Rc), Html>::from( - move |(row, col, dto): (usize, usize, Rc)| { - match SERVER_HEADERS[col] { - "EMPTY" => { - let popup_onclick = popup_onclick.clone(); - let edit_mode_ref = edit_mode_ref.clone(); - html! { - - } + move |(row, col, dto): (usize, usize, Rc)| match SERVER_HEADERS[col] { + "EMPTY" => { + let popup_onclick = popup_onclick.clone(); + let edit_mode_ref = edit_mode_ref.clone(); + html! { + } - "NAME" => html! {&dto.name}, - "PROTOCOL" => html! {&dto.protocol}, - "HOST" => html! {&dto.host}, - "PORT" => html! {&dto.port.as_ref().map_or_else(String::new, ToString::to_string)}, - "TIMEZONE" => html! {&dto.timezone}, - "MESSAGE" => html! {&dto.message}, - "PATH" => html! {&dto.path.as_ref().map_or_else(String::new, ToString::to_string)}, - _ => html! {""}, } - }) + "NAME" => html! {&dto.name}, + "PROTOCOL" => html! {&dto.protocol}, + "HOST" => html! {&dto.host}, + "PORT" => html! {&dto.port.as_ref().map_or_else(String::new, ToString::to_string)}, + "TIMEZONE" => html! {&dto.timezone}, + "MESSAGE" => html! {&dto.message}, + "PATH" => html! {&dto.path.as_ref().map_or_else(String::new, ToString::to_string)}, + _ => html! {""}, + }, + ) }; let table_definition = { @@ -262,15 +257,20 @@ pub fn ApiConfigView() -> Html { let render_data_cell_cb = render_data_cell.clone(); let num_cols = SERVER_HEADERS.len(); use_memo(config_ctx.api_proxy.clone(), move |config| { - config.as_ref().map(|api_proxy| + config.as_ref().map(|api_proxy| { Rc::new(TableDefinition:: { - items: if api_proxy.server.is_empty() { None } else { Some(Rc::new(api_proxy.server.iter().map(|s| Rc::new(s.clone())).collect())) }, + items: if api_proxy.server.is_empty() { + None + } else { + Some(Rc::new(api_proxy.server.iter().map(|s| Rc::new(s.clone())).collect())) + }, num_cols, is_sortable, on_sort, render_header_cell: render_header_cell_cb, render_data_cell: render_data_cell_cb, - })) + }) + }) }) }; diff --git a/frontend/src/app/components/config/config_page.rs b/frontend/src/app/components/config/config_page.rs index cfa507283..53d6bf040 100644 --- a/frontend/src/app/components/config/config_page.rs +++ b/frontend/src/app/components/config/config_page.rs @@ -1,7 +1,12 @@ -use shared::error::{TuliproxError, info_err_res}; -use shared::model::{ApiProxyConfigDto, ConfigApiDto, HdHomeRunConfigDto, IpCheckConfigDto, LibraryConfigDto, LogConfigDto, MainConfigDto, MessagingConfigDto, ProxyConfigDto, ReverseProxyConfigDto, SchedulesConfigDto, SourcesConfigDto, VideoConfigDto, WebUiConfigDto}; -use std::fmt; -use std::str::FromStr; +use shared::{ + error::{info_err_res, TuliproxError}, + model::{ + ApiProxyConfigDto, ConfigApiDto, HdHomeRunConfigDto, IpCheckConfigDto, LibraryConfigDto, LogConfigDto, + MainConfigDto, MessagingConfigDto, ProxyConfigDto, ReverseProxyConfigDto, SchedulesConfigDto, SourcesConfigDto, + VideoConfigDto, WebUiConfigDto, + }, +}; +use std::{fmt, str::FromStr}; pub const LABEL_MAIN_CONFIG: &str = "LABEL.MAIN_CONFIG"; pub const LABEL_API_CONFIG: &str = "LABEL.API_CONFIG"; @@ -112,19 +117,22 @@ pub enum ConfigForm { impl ConfigForm { pub(crate) fn is_modified(&self) -> bool { - matches!(self, ConfigForm::Main(true, _) - | ConfigForm::Api(true, _) - | ConfigForm::ApiProxy(true, _) - | ConfigForm::Log(true, _) - | ConfigForm::Schedules(true, _) - | ConfigForm::Video(true, _) - | ConfigForm::Messaging(true, _) - | ConfigForm::WebUi(true, _) - | ConfigForm::ReverseProxy(true, _) - | ConfigForm::HdHomerun(true, _) - | ConfigForm::Proxy(true, _) - | ConfigForm::IpCheck(true, _) - | ConfigForm::Panel(true, _) - | ConfigForm::Library(true, _)) + matches!( + self, + ConfigForm::Main(true, _) + | ConfigForm::Api(true, _) + | ConfigForm::ApiProxy(true, _) + | ConfigForm::Log(true, _) + | ConfigForm::Schedules(true, _) + | ConfigForm::Video(true, _) + | ConfigForm::Messaging(true, _) + | ConfigForm::WebUi(true, _) + | ConfigForm::ReverseProxy(true, _) + | ConfigForm::HdHomerun(true, _) + | ConfigForm::Proxy(true, _) + | ConfigForm::IpCheck(true, _) + | ConfigForm::Panel(true, _) + | ConfigForm::Library(true, _) + ) } } diff --git a/frontend/src/app/components/config/config_update.rs b/frontend/src/app/components/config/config_update.rs index f8ad439ae..8ce0410dd 100644 --- a/frontend/src/app/components/config/config_update.rs +++ b/frontend/src/app/components/config/config_update.rs @@ -1,5 +1,5 @@ -use shared::model::{ConfigDto}; use crate::app::components::config::config_page::ConfigForm; +use shared::model::ConfigDto; macro_rules! set_config_field { ($main_config:expr, $config:expr, $field:ident) => { @@ -24,17 +24,19 @@ pub fn update_config(config: &mut ConfigDto, forms: Vec) { } else { config.schedules = schedules_cfg.schedules.clone(); } - }, - ConfigForm::Video(_, mut video_cfg) => set_config_field!(config, video_cfg, video), + } + ConfigForm::Video(_, mut video_cfg) => set_config_field!(config, video_cfg, video), ConfigForm::Messaging(_, mut messaging_cfg) => set_config_field!(config, messaging_cfg, messaging), ConfigForm::WebUi(_, mut web_ui_cfg) => set_config_field!(config, web_ui_cfg, web_ui), - ConfigForm::ReverseProxy(_, mut reverse_proxy_cfg) => set_config_field!(config, reverse_proxy_cfg, reverse_proxy), + ConfigForm::ReverseProxy(_, mut reverse_proxy_cfg) => { + set_config_field!(config, reverse_proxy_cfg, reverse_proxy) + } ConfigForm::HdHomerun(_, mut hdhr_cfg) => set_config_field!(config, hdhr_cfg, hdhomerun), ConfigForm::Proxy(_, mut proxy_cfg) => set_config_field!(config, proxy_cfg, proxy), ConfigForm::IpCheck(_, mut ipcheck_cfg) => set_config_field!(config, ipcheck_cfg, ipcheck), ConfigForm::Library(_, mut library_cfg) => set_config_field!(config, library_cfg, library), - ConfigForm::Panel(_, _) => {}, - ConfigForm::ApiProxy(_, _) => {}, + ConfigForm::Panel(_, _) => {} + ConfigForm::ApiProxy(_, _) => {} } } } diff --git a/frontend/src/app/components/config/config_view.rs b/frontend/src/app/components/config/config_view.rs index 180c1a747..a48091887 100644 --- a/frontend/src/app/components/config/config_view.rs +++ b/frontend/src/app/components/config/config_view.rs @@ -1,17 +1,31 @@ -use crate::app::components::config::config_page::{ConfigForm, ConfigPage, LABEL_API_CONFIG, LABEL_HDHOMERUN_CONFIG, LABEL_IP_CHECK_CONFIG, LABEL_LIBRARY_CONFIG, LABEL_LOG_CONFIG, LABEL_MAIN_CONFIG, LABEL_MESSAGING_CONFIG, LABEL_PANEL_CONFIG, LABEL_PROXY_CONFIG, LABEL_REVERSE_PROXY_CONFIG, LABEL_SCHEDULES_CONFIG, LABEL_VIDEO_CONFIG, LABEL_WEB_UI_CONFIG}; -use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::config::{ApiConfigView, HdHomerunConfigView, IpCheckConfigView, LibraryConfigView, LogConfigView, MainConfigView, MessagingConfigView, PanelConfigView, ProxyConfigView, ReverseProxyConfigView, SchedulesConfigView, VideoConfigView, WebUiConfigView}; -use crate::app::components::{Card, TabItem, TabSet, TextButton}; -use crate::html_if; -use std::str::FromStr; +use crate::{ + app::{ + components::{ + config::{ + config_page::{ + ConfigForm, ConfigPage, LABEL_API_CONFIG, LABEL_HDHOMERUN_CONFIG, LABEL_IP_CHECK_CONFIG, + LABEL_LIBRARY_CONFIG, LABEL_LOG_CONFIG, LABEL_MAIN_CONFIG, LABEL_MESSAGING_CONFIG, + LABEL_PANEL_CONFIG, LABEL_PROXY_CONFIG, LABEL_REVERSE_PROXY_CONFIG, LABEL_SCHEDULES_CONFIG, + LABEL_VIDEO_CONFIG, LABEL_WEB_UI_CONFIG, + }, + config_update::update_config, + config_view_context::ConfigViewContext, + ApiConfigView, HdHomerunConfigView, IpCheckConfigView, LibraryConfigView, LogConfigView, + MainConfigView, MessagingConfigView, PanelConfigView, ProxyConfigView, ReverseProxyConfigView, + SchedulesConfigView, VideoConfigView, WebUiConfigView, + }, + Card, TabItem, TabSet, TextButton, + }, + ConfigContext, + }, + hooks::use_service_context, + html_if, +}; use log::warn; -use yew::platform::spawn_local; -use yew::prelude::*; -use yew_i18n::use_translation; use shared::model::{ApiProxyConfigDto, ConfigDto, SourcesConfigDto}; -use crate::app::components::config::config_update::update_config; -use crate::app::{ConfigContext}; -use crate::hooks::use_service_context; +use std::str::FromStr; +use yew::{platform::spawn_local, prelude::*}; +use yew_i18n::use_translation; const LABEL_CONFIG: &str = "LABEL.CONFIG"; const LABEL_EDIT: &str = "LABEL.EDIT"; @@ -98,12 +112,27 @@ pub fn ConfigView() -> Html { use_memo((form_state, edit_value, translate.clone()), move |(forms, editing, translate)| { let forms: &ConfigFormState = forms; - let modified_pages = collect_modified!(forms, [ - main, api, log, schedules, video, messaging, web_ui, - reverse_proxy, hd_homerun, proxy, ipcheck, panel, library - ]).iter() - .map(config_form_to_config_page) - .collect::>(); + let modified_pages = collect_modified!( + forms, + [ + main, + api, + log, + schedules, + video, + messaging, + web_ui, + reverse_proxy, + hd_homerun, + proxy, + ipcheck, + panel, + library + ] + ) + .iter() + .map(config_form_to_config_page) + .collect::>(); let tab_configs = vec![ (ConfigPage::Main, LABEL_MAIN_CONFIG, html! { }, "MainConfig"), @@ -112,7 +141,12 @@ pub fn ConfigView() -> Html { (ConfigPage::Schedules, LABEL_SCHEDULES_CONFIG, html! { }, "SchedulesConfig"), (ConfigPage::Messaging, LABEL_MESSAGING_CONFIG, html! { }, "MessagingConfig"), (ConfigPage::WebUi, LABEL_WEB_UI_CONFIG, html! { }, "WebUiConfig"), - (ConfigPage::ReverseProxy, LABEL_REVERSE_PROXY_CONFIG, html! { }, "ReverseProxyConfig"), + ( + ConfigPage::ReverseProxy, + LABEL_REVERSE_PROXY_CONFIG, + html! { }, + "ReverseProxyConfig", + ), (ConfigPage::HdHomerun, LABEL_HDHOMERUN_CONFIG, html! { }, "HdHomerunConfig"), (ConfigPage::Proxy, LABEL_PROXY_CONFIG, html! { }, "ProxyConfig"), (ConfigPage::IpCheck, LABEL_IP_CHECK_CONFIG, html! { }, "IpCheckConfig"), @@ -122,17 +156,24 @@ pub fn ConfigView() -> Html { ]; let editing = *editing; - tab_configs.into_iter().map(|(page, label, children, icon)| { - let is_modified = editing && modified_pages.contains(&page); - TabItem { - id: page.to_string(), - title: translate.t(label), - icon: icon.to_string(), - children, - active_class: if is_modified { Some("tp__tab__modified__active".to_string()) } else { None }, - inactive_class: if is_modified { Some("tp__tab__modified__inactive".to_string()) } else { None }, - } - }).collect::>() + tab_configs + .into_iter() + .map(|(page, label, children, icon)| { + let is_modified = editing && modified_pages.contains(&page); + TabItem { + id: page.to_string(), + title: translate.t(label), + icon: icon.to_string(), + children, + active_class: if is_modified { Some("tp__tab__modified__active".to_string()) } else { None }, + inactive_class: if is_modified { + Some("tp__tab__modified__inactive".to_string()) + } else { + None + }, + } + }) + .collect::>() }) }; @@ -152,10 +193,25 @@ pub fn ConfigView() -> Html { Callback::from(move |_| { let forms = &*get_form_state; - let modified_forms: Vec = collect_modified!(forms, [ - main, api, api_proxy, log, schedules, video, messaging, web_ui, - reverse_proxy, hd_homerun, proxy, ipcheck, panel, library - ]); + let modified_forms: Vec = collect_modified!( + forms, + [ + main, + api, + api_proxy, + log, + schedules, + video, + messaging, + web_ui, + reverse_proxy, + hd_homerun, + proxy, + ipcheck, + panel, + library + ] + ); if modified_forms.is_empty() { set_edit_mode.set(false); @@ -176,10 +232,8 @@ pub fn ConfigView() -> Html { let mut modified_main_dto: Option = None; if !modified_main_forms.is_empty() { - let mut config_dto = config_ctx - .config - .as_ref() - .map_or_else(ConfigDto::default, |app_cfg| app_cfg.config.clone()); + let mut config_dto = + config_ctx.config.as_ref().map_or_else(ConfigDto::default, |app_cfg| app_cfg.config.clone()); update_config(&mut config_dto, modified_main_forms); if let Err(err) = config_dto.prepare(false) { services.toastr.error(err.to_string()); @@ -288,7 +342,6 @@ pub fn ConfigView() -> Html { }) }; - let handle_update_content = { let services = services_ctx.clone(); let translate = translate.clone(); @@ -306,10 +359,7 @@ pub fn ConfigView() -> Html { }) }; - let context = ConfigViewContext { - edit_mode: edit_mode.clone(), - on_form_change: on_form_change.clone(), - }; + let context = ConfigViewContext { edit_mode: edit_mode.clone(), on_form_change: on_form_change.clone() }; let geo_ip_enabled = config_ctx.config.as_ref().is_some_and(|c| c.config.is_geoip_enabled()); diff --git a/frontend/src/app/components/config/config_view_context.rs b/frontend/src/app/components/config/config_view_context.rs index 700b81b73..e23a7c162 100644 --- a/frontend/src/app/components/config/config_view_context.rs +++ b/frontend/src/app/components/config/config_view_context.rs @@ -1,8 +1,8 @@ -use yew::{Callback, UseStateHandle}; use crate::app::components::config::config_page::ConfigForm; +use yew::{Callback, UseStateHandle}; #[derive(Clone, PartialEq)] pub struct ConfigViewContext { pub edit_mode: UseStateHandle, pub on_form_change: Callback, -} \ No newline at end of file +} diff --git a/frontend/src/app/components/config/hdhomerun_config_view.rs b/frontend/src/app/components/config/hdhomerun_config_view.rs index 2033a79e7..fa74335d9 100644 --- a/frontend/src/app/components/config/hdhomerun_config_view.rs +++ b/frontend/src/app/components/config/hdhomerun_config_view.rs @@ -1,20 +1,27 @@ +use crate::{ + app::{ + components::{ + config::{ + config_page::{ConfigForm, LABEL_HDHOMERUN_CONFIG}, + config_view_context::ConfigViewContext, + hdhomerun_device_view::HdHomerunDeviceView, + }, + Card, NoContent, TextButton, + }, + context::ConfigContext, + }, + config_field_bool, edit_field_bool, generate_form_reducer, html_if, +}; use log::error; +use shared::model::{HdHomeRunConfigDto, HdHomeRunDeviceConfigDto}; use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::{HdHomeRunDeviceConfigDto, HdHomeRunConfigDto}; -use crate::app::components::{Card, NoContent, TextButton}; -use crate::app::context::ConfigContext; -use crate::{config_field_bool, edit_field_bool, generate_form_reducer, html_if}; -use crate::app::components::config::config_page::{ConfigForm, LABEL_HDHOMERUN_CONFIG}; -use crate::app::components::config::hdhomerun_device_view::{HdHomerunDeviceView}; -use crate::app::components::config::config_view_context::ConfigViewContext; const LABEL_ENABLED: &str = "LABEL.ENABLED"; const LABEL_DEVICE_AUTH: &str = "LABEL.DEVICE_AUTH"; -const LABEL_DEVICES: &str ="LABEL.DEVICES"; -const LABEL_SSDP_DISCOVERY: &str ="LABEL.SSDP_DISCOVERY"; -const LABEL_PROPRIETARY_DISCOVERY: &str ="LABEL.PROPRIETARY_DISCOVERY"; - +const LABEL_DEVICES: &str = "LABEL.DEVICES"; +const LABEL_SSDP_DISCOVERY: &str = "LABEL.SSDP_DISCOVERY"; +const LABEL_PROPRIETARY_DISCOVERY: &str = "LABEL.PROPRIETARY_DISCOVERY"; generate_form_reducer!( state: HdHomeRunConfigFormState { form: HdHomeRunConfigDto }, @@ -34,9 +41,8 @@ pub fn HdHomerunConfigView() -> Html { let config_ctx = use_context::().expect("ConfigContext not found"); let config_view_ctx = use_context::().expect("ConfigViewContext not found"); - let form_state: UseReducerHandle = use_reducer(|| { - HdHomeRunConfigFormState { form: HdHomeRunConfigDto::default(), modified: false } - }); + let form_state: UseReducerHandle = + use_reducer(|| HdHomeRunConfigFormState { form: HdHomeRunConfigDto::default(), modified: false }); { let on_form_change = config_view_ctx.on_form_change.clone(); @@ -48,10 +54,7 @@ pub fn HdHomerunConfigView() -> Html { { let form_state = form_state.clone(); - let hdhr_config = config_ctx - .config - .as_ref() - .and_then(|c| c.config.hdhomerun.clone()); + let hdhr_config = config_ctx.config.as_ref().and_then(|c| c.config.hdhomerun.clone()); use_effect_with((hdhr_config, config_view_ctx.edit_mode.clone()), move |(hdhr_cfg, _mode)| { if let Some(hdhr) = hdhr_cfg { @@ -79,10 +82,7 @@ pub fn HdHomerunConfigView() -> Html { Callback::from(move |_| { let mut devices = form_state.form.devices.clone(); let mut new_device = HdHomeRunDeviceConfigDto::default(); - let next_port = devices.iter() - .map(|d| d.port) - .max() - .unwrap_or(8901) + 1; + let next_port = devices.iter().map(|d| d.port).max().unwrap_or(8901) + 1; new_device.port = next_port; new_device.name = format!("hdhr_{next_port}"); if let Err(err) = new_device.prepare(devices.len() as u8, false) { @@ -96,10 +96,10 @@ pub fn HdHomerunConfigView() -> Html { let handle_remove_device = { let form_state = form_state.clone(); Callback::from(move |device_idx| { - let mut devices = form_state.form.devices.clone(); - devices.remove(device_idx); - form_state.dispatch(HdHomeRunConfigFormAction::Devices(devices)); - }) + let mut devices = form_state.form.devices.clone(); + devices.remove(device_idx); + form_state.dispatch(HdHomeRunConfigFormAction::Devices(devices)); + }) }; let render_empty = || { @@ -119,7 +119,7 @@ pub fn HdHomerunConfigView() -> Html { if devices.is_empty() { render_empty() } else { - html!{ for devices.iter().enumerate().map(|(idx, entry)| html! { + html! { for devices.iter().enumerate().map(|(idx, entry)| html! { })} @@ -161,4 +161,4 @@ pub fn HdHomerunConfigView() -> Html { } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/config/hdhomerun_device_view.rs b/frontend/src/app/components/config/hdhomerun_device_view.rs index 6fd6945d2..919f14ad1 100644 --- a/frontend/src/app/components/config/hdhomerun_device_view.rs +++ b/frontend/src/app/components/config/hdhomerun_device_view.rs @@ -1,8 +1,10 @@ +use crate::{ + app::components::{Card, CollapsePanel, TextButton}, + config_field, edit_field_number_u16, edit_field_number_u8, edit_field_text, generate_form_reducer, html_if, +}; +use shared::model::HdHomeRunDeviceConfigDto; use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::{HdHomeRunDeviceConfigDto}; -use crate::app::components::{Card, CollapsePanel, TextButton}; -use crate::{config_field, edit_field_number_u16, edit_field_number_u8, edit_field_text, generate_form_reducer, html_if}; generate_form_reducer!( state: HdHomeRunDeviceConfigFormState { form: Box }, @@ -36,9 +38,8 @@ pub struct HdHomerunDeviceViewProps { pub fn HdHomerunDeviceView(props: &HdHomerunDeviceViewProps) -> Html { let translate = use_translation(); - let form_state: UseReducerHandle = use_reducer(|| { - HdHomeRunDeviceConfigFormState { form: Box::new(props.device.clone()), modified: false } - }); + let form_state: UseReducerHandle = + use_reducer(|| HdHomeRunDeviceConfigFormState { form: Box::new(props.device.clone()), modified: false }); { let on_form_change = props.on_form_change.clone(); @@ -106,4 +107,4 @@ pub fn HdHomerunDeviceView(props: &HdHomerunDeviceViewProps) -> Html { }; render_device(&form_state) -} \ No newline at end of file +} diff --git a/frontend/src/app/components/config/ipcheck_config_view.rs b/frontend/src/app/components/config/ipcheck_config_view.rs index 74f6635a5..63fbfe1b5 100644 --- a/frontend/src/app/components/config/ipcheck_config_view.rs +++ b/frontend/src/app/components/config/ipcheck_config_view.rs @@ -1,15 +1,21 @@ +use crate::{ + app::{ + components::config::{ + config_page::{ConfigForm, LABEL_IP_CHECK_CONFIG}, + config_view_context::ConfigViewContext, + }, + context::ConfigContext, + }, + config_field_empty, config_field_optional, edit_field_text_option, generate_form_reducer, +}; +use shared::model::IpCheckConfigDto; use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::{IpCheckConfigDto}; -use crate::app::context::ConfigContext; -use crate::{config_field_empty, config_field_optional, edit_field_text_option, generate_form_reducer}; -use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::config::config_page::{ConfigForm, LABEL_IP_CHECK_CONFIG}; -const LABEL_URL: &str = "LABEL.URL"; -const LABEL_URL_IPV4: &str = "LABEL.URL_IPV4"; -const LABEL_URL_IPV6: &str = "LABEL.URL_IPV6"; -const LABEL_PATTERN_IPV4: &str = "LABEL.PATTERN_IPV4"; -const LABEL_PATTERN_IPV6: &str = "LABEL.PATTERN_IPV6"; +const LABEL_URL: &str = "LABEL.URL"; +const LABEL_URL_IPV4: &str = "LABEL.URL_IPV4"; +const LABEL_URL_IPV6: &str = "LABEL.URL_IPV6"; +const LABEL_PATTERN_IPV4: &str = "LABEL.PATTERN_IPV4"; +const LABEL_PATTERN_IPV6: &str = "LABEL.PATTERN_IPV6"; generate_form_reducer!( state: IpCheckConfigFormState { form: IpCheckConfigDto }, @@ -29,9 +35,8 @@ pub fn IpCheckConfigView() -> Html { let config_ctx = use_context::().expect("ConfigContext not found"); let config_view_ctx = use_context::().expect("ConfigViewContext not found"); - let form_state: UseReducerHandle = use_reducer(|| { - IpCheckConfigFormState { form: IpCheckConfigDto::default(), modified: false } - }); + let form_state: UseReducerHandle = + use_reducer(|| IpCheckConfigFormState { form: IpCheckConfigDto::default(), modified: false }); { let on_form_change = config_view_ctx.on_form_change.clone(); @@ -43,10 +48,7 @@ pub fn IpCheckConfigView() -> Html { { let form_state = form_state.clone(); - let ipcheck_config = config_ctx - .config - .as_ref() - .and_then(|c| c.config.ipcheck.clone()); + let ipcheck_config = config_ctx.config.as_ref().and_then(|c| c.config.ipcheck.clone()); use_effect_with((ipcheck_config, config_view_ctx.edit_mode.clone()), move |(ipcheck_cfg, _mode)| { if let Some(ipcheck) = ipcheck_cfg { @@ -116,4 +118,4 @@ pub fn IpCheckConfigView() -> Html { } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/config/library_config_view.rs b/frontend/src/app/components/config/library_config_view.rs index 5cc59f39c..f929655be 100644 --- a/frontend/src/app/components/config/library_config_view.rs +++ b/frontend/src/app/components/config/library_config_view.rs @@ -1,14 +1,23 @@ -use crate::app::components::config::config_page::{ConfigForm, LABEL_LIBRARY_CONFIG}; -use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::input::Input; -use crate::app::components::select::Select; -use crate::app::components::{Card, Chip, DropDownOption, DropDownSelection, IconButton, ToggleSwitch}; -use crate::app::context::ConfigContext; -use crate::{config_field, config_field_bool, config_field_child, config_field_optional, - edit_field_bool, edit_field_list, edit_field_number, edit_field_number_u64, edit_field_text, edit_field_text_option, generate_form_reducer}; -use shared::model::{LibraryConfigDto, LibraryContentType, LibraryMetadataConfigDto, - LibraryMetadataReadConfigDto, LibraryPlaylistConfigDto, LibraryScanDirectoryDto, - LibraryTmdbConfigDto, LibraryMetadataFormat}; +use crate::{ + app::{ + components::{ + config::{ + config_page::{ConfigForm, LABEL_LIBRARY_CONFIG}, + config_view_context::ConfigViewContext, + }, + input::Input, + select::Select, + Card, Chip, DropDownOption, DropDownSelection, IconButton, ToggleSwitch, + }, + context::ConfigContext, + }, + config_field, config_field_bool, config_field_child, config_field_optional, edit_field_bool, edit_field_list, + edit_field_number, edit_field_number_u64, edit_field_text, edit_field_text_option, generate_form_reducer, +}; +use shared::model::{ + LibraryConfigDto, LibraryContentType, LibraryMetadataConfigDto, LibraryMetadataFormat, + LibraryMetadataReadConfigDto, LibraryPlaylistConfigDto, LibraryScanDirectoryDto, LibraryTmdbConfigDto, +}; use std::rc::Rc; use yew::prelude::*; use yew_i18n::use_translation; @@ -45,7 +54,6 @@ const TYPE_AUTO: &str = "Auto"; const TYPE_MOVIE: &str = "Movie"; const TYPE_SERIES: &str = "Series"; - generate_form_reducer!( state: LibraryConfigFormState { form: LibraryConfigDto }, action_name: LibraryConfigFormAction, @@ -103,25 +111,21 @@ pub fn LibraryConfigView() -> Html { let config_ctx = use_context::().expect("ConfigContext not found"); let config_view_ctx = use_context::().expect("ConfigViewContext not found"); - let form_state: UseReducerHandle = use_reducer(|| { - LibraryConfigFormState { form: LibraryConfigDto::default(), modified: false } - }); + let form_state: UseReducerHandle = + use_reducer(|| LibraryConfigFormState { form: LibraryConfigDto::default(), modified: false }); - let playlist_state: UseReducerHandle = use_reducer(|| { - LibraryPlaylistConfigFormState { form: LibraryPlaylistConfigDto::default(), modified: false } - }); + let playlist_state: UseReducerHandle = + use_reducer(|| LibraryPlaylistConfigFormState { form: LibraryPlaylistConfigDto::default(), modified: false }); - let metadata_state: UseReducerHandle = use_reducer(|| { - LibraryMetadataConfigFormState { form: LibraryMetadataConfigDto::default(), modified: false } - }); + let metadata_state: UseReducerHandle = + use_reducer(|| LibraryMetadataConfigFormState { form: LibraryMetadataConfigDto::default(), modified: false }); let metadata_read_state: UseReducerHandle = use_reducer(|| { LibraryMetadataReadConfigFormState { form: LibraryMetadataReadConfigDto::default(), modified: false } }); - let tmdb_state: UseReducerHandle = use_reducer(|| { - LibraryTmdbConfigFormState { form: LibraryTmdbConfigDto::default(), modified: false } - }); + let tmdb_state: UseReducerHandle = + use_reducer(|| LibraryTmdbConfigFormState { form: LibraryTmdbConfigDto::default(), modified: false }); { let form_state = form_state.clone(); @@ -136,13 +140,15 @@ pub fn LibraryConfigView() -> Html { form_state.dispatch(LibraryConfigFormAction::SetAll(library.clone())); playlist_state.dispatch(LibraryPlaylistConfigFormAction::SetAll(library.playlist.clone())); metadata_state.dispatch(LibraryMetadataConfigFormAction::SetAll(library.metadata.clone())); - metadata_read_state.dispatch(LibraryMetadataReadConfigFormAction::SetAll(library.metadata.read_existing.clone())); + metadata_read_state + .dispatch(LibraryMetadataReadConfigFormAction::SetAll(library.metadata.read_existing.clone())); tmdb_state.dispatch(LibraryTmdbConfigFormAction::SetAll(library.metadata.tmdb.clone())); } else { form_state.dispatch(LibraryConfigFormAction::SetAll(LibraryConfigDto::default())); playlist_state.dispatch(LibraryPlaylistConfigFormAction::SetAll(LibraryPlaylistConfigDto::default())); metadata_state.dispatch(LibraryMetadataConfigFormAction::SetAll(LibraryMetadataConfigDto::default())); - metadata_read_state.dispatch(LibraryMetadataReadConfigFormAction::SetAll(LibraryMetadataReadConfigDto::default())); + metadata_read_state + .dispatch(LibraryMetadataReadConfigFormAction::SetAll(LibraryMetadataReadConfigDto::default())); tmdb_state.dispatch(LibraryTmdbConfigFormAction::SetAll(LibraryTmdbConfigDto::default())); } || () @@ -158,13 +164,7 @@ pub fn LibraryConfigView() -> Html { let tmdb_state = tmdb_state.clone(); use_effect_with( - ( - form_state, - playlist_state, - metadata_state, - metadata_read_state, - tmdb_state, - ), + (form_state, playlist_state, metadata_state, metadata_read_state, tmdb_state), move |(form, playlist, metadata, metadata_read, tmdb)| { let mut new_form = form.form.clone(); new_form.playlist = playlist.form.clone(); @@ -172,27 +172,35 @@ pub fn LibraryConfigView() -> Html { new_form.metadata.read_existing = metadata_read.form.clone(); new_form.metadata.tmdb = tmdb.form.clone(); - let modified = form.modified - || playlist.modified - || metadata.modified - || metadata_read.modified - || tmdb.modified; + let modified = + form.modified || playlist.modified || metadata.modified || metadata_read.modified || tmdb.modified; on_form_change.emit(ConfigForm::Library(modified, new_form)); }, ); } - let get_content_type_options = { let translate = translate.clone(); - Callback::from(move |content_type: LibraryContentType| + Callback::from(move |content_type: LibraryContentType| { vec![ - DropDownOption { id: TYPE_AUTO.to_string(), label: html! { translate.t(LABEL_AUTO) }, selected: content_type == LibraryContentType::Auto }, - DropDownOption { id: TYPE_MOVIE.to_string(), label: html! { translate.t(LABEL_MOVIE) }, selected: content_type == LibraryContentType::Movie }, - DropDownOption { id: TYPE_SERIES.to_string(), label: html! { translate.t(LABEL_SERIES) }, selected: content_type == LibraryContentType::Series }, + DropDownOption { + id: TYPE_AUTO.to_string(), + label: html! { translate.t(LABEL_AUTO) }, + selected: content_type == LibraryContentType::Auto, + }, + DropDownOption { + id: TYPE_MOVIE.to_string(), + label: html! { translate.t(LABEL_MOVIE) }, + selected: content_type == LibraryContentType::Movie, + }, + DropDownOption { + id: TYPE_SERIES.to_string(), + label: html! { translate.t(LABEL_SERIES) }, + selected: content_type == LibraryContentType::Series, + }, ] - ) + }) }; let handle_path_change = { @@ -200,7 +208,7 @@ pub fn LibraryConfigView() -> Html { Callback::from(move |(idx, value): (usize, String)| { let mut scan_directories = form.form.scan_directories.clone(); if idx >= scan_directories.len() { - return; + return; } scan_directories[idx].path = value; form.dispatch(LibraryConfigFormAction::ScanDirectories(scan_directories)); @@ -255,7 +263,7 @@ pub fn LibraryConfigView() -> Html { Callback::from(move |idx: usize| { let mut current_list = form_state.form.scan_directories.clone(); if idx >= current_list.len() { - return; + return; } current_list.remove(idx); form_state.dispatch(LibraryConfigFormAction::ScanDirectories(current_list)); @@ -273,36 +281,40 @@ pub fn LibraryConfigView() -> Html { }) }; - let render_extensions = |extensions: &Vec| html! { - - { config_field_child!(translate.t(LABEL_SUPPORTED_EXTENSIONS), { - html! { -
- { html! { for extensions.iter().map(|t| html! { }) } } -
- }})} -
+ let render_extensions = |extensions: &Vec| { + html! { + + { config_field_child!(translate.t(LABEL_SUPPORTED_EXTENSIONS), { + html! { +
+ { html! { for extensions.iter().map(|t| html! { }) } } +
+ }})} +
+ } }; - let render_scan_directories_view = |directories: &Vec| html! { - <> -

{translate.t(LABEL_SCAN_DIRECTORIES)}

-
    - { for directories.iter().map(|dir| html! { -
  • {&dir.path} - {format!("({}, {}, {}: {})", - if dir.enabled { translate.t("LABEL.ENABLED") } else {translate.t("LABEL.DISABLED")}, - if dir.recursive { translate.t("LABEL.RECURSIVE") } else {translate.t("LABEL.NON_RECURSIVE")}, - translate.t(LABEL_CONTENT_TYPE), - match dir.content_type { - LibraryContentType::Auto => translate.t(LABEL_AUTO), - LibraryContentType::Movie => translate.t(LABEL_MOVIE), - LibraryContentType::Series => translate.t(LABEL_SERIES), - })} -
  • - }) } -
- + let render_scan_directories_view = |directories: &Vec| { + html! { + <> +

{translate.t(LABEL_SCAN_DIRECTORIES)}

+
    + { for directories.iter().map(|dir| html! { +
  • {&dir.path} + {format!("({}, {}, {}: {})", + if dir.enabled { translate.t("LABEL.ENABLED") } else {translate.t("LABEL.DISABLED")}, + if dir.recursive { translate.t("LABEL.RECURSIVE") } else {translate.t("LABEL.NON_RECURSIVE")}, + translate.t(LABEL_CONTENT_TYPE), + match dir.content_type { + LibraryContentType::Auto => translate.t(LABEL_AUTO), + LibraryContentType::Movie => translate.t(LABEL_MOVIE), + LibraryContentType::Series => translate.t(LABEL_SERIES), + })} +
  • + }) } +
+ + } }; let render_scan_directories_edit = || { html! { @@ -404,70 +416,72 @@ pub fn LibraryConfigView() -> Html { } }; - let render_edit_mode = || html! { - <> -
- { edit_field_bool!(form_state, translate.t(LABEL_ENABLED), enabled, LibraryConfigFormAction::Enabled) } - { render_scan_directories_edit() } -
-
- - { edit_field_list!(form_state, translate.t(LABEL_SUPPORTED_EXTENSIONS), supported_extensions, LibraryConfigFormAction::SupportedExtensions, translate.t(LABEL_ADD_EXTENSION)) } - + let render_edit_mode = || { + html! { + <> +
+ { edit_field_bool!(form_state, translate.t(LABEL_ENABLED), enabled, LibraryConfigFormAction::Enabled) } + { render_scan_directories_edit() } +
+
+ + { edit_field_list!(form_state, translate.t(LABEL_SUPPORTED_EXTENSIONS), supported_extensions, LibraryConfigFormAction::SupportedExtensions, translate.t(LABEL_ADD_EXTENSION)) } + - -

{translate.t(LABEL_METADATA)}

- { edit_field_text!(metadata_state, translate.t(LABEL_METADATA_PATH), path, LibraryMetadataConfigFormAction::Path) } - { edit_field_bool!(metadata_state, translate.t(LABEL_FALLBACK_TO_FILENAME), fallback_to_filename, LibraryMetadataConfigFormAction::FallbackToFilename) } - { config_field_child!(translate.t(LABEL_FORMATS), { - let metadata_state = metadata_state.clone(); - let formats = metadata_state.form.formats.clone(); - html! { -
- { for formats.iter().map(|f| { - let metadata_state = metadata_state.clone(); - let f = *f; - html! { } - }) } - if !formats.contains(&LibraryMetadataFormat::Nfo) { - - } -
- } - }) } -
+ +

{translate.t(LABEL_METADATA)}

+ { edit_field_text!(metadata_state, translate.t(LABEL_METADATA_PATH), path, LibraryMetadataConfigFormAction::Path) } + { edit_field_bool!(metadata_state, translate.t(LABEL_FALLBACK_TO_FILENAME), fallback_to_filename, LibraryMetadataConfigFormAction::FallbackToFilename) } + { config_field_child!(translate.t(LABEL_FORMATS), { + let metadata_state = metadata_state.clone(); + let formats = metadata_state.form.formats.clone(); + html! { +
+ { for formats.iter().map(|f| { + let metadata_state = metadata_state.clone(); + let f = *f; + html! { } + }) } + if !formats.contains(&LibraryMetadataFormat::Nfo) { + + } +
+ } + }) } +
- -

{translate.t(LABEL_READ_EXISTING)}

- { edit_field_bool!(metadata_read_state, translate.t(LABEL_KODI), kodi, LibraryMetadataReadConfigFormAction::Kodi) } - { edit_field_bool!(metadata_read_state, translate.t(LABEL_JELLYFIN), jellyfin, LibraryMetadataReadConfigFormAction::Jellyfin) } - { edit_field_bool!(metadata_read_state, translate.t(LABEL_PLEX), plex, LibraryMetadataReadConfigFormAction::Plex) } -
+ +

{translate.t(LABEL_READ_EXISTING)}

+ { edit_field_bool!(metadata_read_state, translate.t(LABEL_KODI), kodi, LibraryMetadataReadConfigFormAction::Kodi) } + { edit_field_bool!(metadata_read_state, translate.t(LABEL_JELLYFIN), jellyfin, LibraryMetadataReadConfigFormAction::Jellyfin) } + { edit_field_bool!(metadata_read_state, translate.t(LABEL_PLEX), plex, LibraryMetadataReadConfigFormAction::Plex) } +
- -

{translate.t(LABEL_TMDB)}

- { edit_field_bool!(tmdb_state, translate.t(LABEL_ENABLED), enabled, LibraryTmdbConfigFormAction::Enabled) } - { edit_field_text_option!(tmdb_state, translate.t(LABEL_API_KEY), api_key, LibraryTmdbConfigFormAction::ApiKey, true) } - { edit_field_number_u64!(tmdb_state, translate.t(LABEL_RATE_LIMIT_MS), rate_limit_ms, LibraryTmdbConfigFormAction::RateLimitMs) } - { edit_field_number!(tmdb_state, translate.t(LABEL_CACHE_DURATION_DAYS), cache_duration_days, LibraryTmdbConfigFormAction::CacheDurationDays) } - { edit_field_text!(tmdb_state, translate.t(LABEL_LANGUAGE), language, LibraryTmdbConfigFormAction::Language) } -
+ +

{translate.t(LABEL_TMDB)}

+ { edit_field_bool!(tmdb_state, translate.t(LABEL_ENABLED), enabled, LibraryTmdbConfigFormAction::Enabled) } + { edit_field_text_option!(tmdb_state, translate.t(LABEL_API_KEY), api_key, LibraryTmdbConfigFormAction::ApiKey, true) } + { edit_field_number_u64!(tmdb_state, translate.t(LABEL_RATE_LIMIT_MS), rate_limit_ms, LibraryTmdbConfigFormAction::RateLimitMs) } + { edit_field_number!(tmdb_state, translate.t(LABEL_CACHE_DURATION_DAYS), cache_duration_days, LibraryTmdbConfigFormAction::CacheDurationDays) } + { edit_field_text!(tmdb_state, translate.t(LABEL_LANGUAGE), language, LibraryTmdbConfigFormAction::Language) } +
- -

{translate.t(LABEL_PLAYLIST)}

- { edit_field_text!(playlist_state, translate.t(LABEL_MOVIE_CATEGORY), movie_category, LibraryPlaylistConfigFormAction::MovieCategory) } - { edit_field_text!(playlist_state, translate.t(LABEL_SERIES_CATEGORY), series_category, LibraryPlaylistConfigFormAction::SeriesCategory) } -
-
- + +

{translate.t(LABEL_PLAYLIST)}

+ { edit_field_text!(playlist_state, translate.t(LABEL_MOVIE_CATEGORY), movie_category, LibraryPlaylistConfigFormAction::MovieCategory) } + { edit_field_text!(playlist_state, translate.t(LABEL_SERIES_CATEGORY), series_category, LibraryPlaylistConfigFormAction::SeriesCategory) } +
+
+ + } }; html! { diff --git a/frontend/src/app/components/config/log_config_view.rs b/frontend/src/app/components/config/log_config_view.rs index cb039266e..fb39af8b7 100644 --- a/frontend/src/app/components/config/log_config_view.rs +++ b/frontend/src/app/components/config/log_config_view.rs @@ -1,16 +1,24 @@ +use crate::{ + app::{ + components::{ + config::{ + config_page::{ConfigForm, LABEL_LOG_CONFIG}, + config_view_context::ConfigViewContext, + }, + Card, Chip, RadioButtonGroup, + }, + context::ConfigContext, + }, + config_field_bool, config_field_child, edit_field_bool, generate_form_reducer, +}; +use shared::model::LogConfigDto; use std::rc::Rc; use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::{LogConfigDto}; -use crate::app::context::ConfigContext; -use crate::{config_field_bool, config_field_child, edit_field_bool, generate_form_reducer}; -use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::config::config_page::{ConfigForm, LABEL_LOG_CONFIG}; -use crate::app::components::{Card, Chip, RadioButtonGroup}; -const LABEL_LOG_LEVEL: &str = "LABEL.LOG_LEVEL"; -const LABEL_LOG_ACTIVE_USER: &str = "LABEL.LOG_ACTIVE_USER"; -const LABEL_LOG_SANITIZE_SENSITIVE_INFO: &str = "LABEL.SANITIZE_SENSITIVE_INFO"; +const LABEL_LOG_LEVEL: &str = "LABEL.LOG_LEVEL"; +const LABEL_LOG_ACTIVE_USER: &str = "LABEL.LOG_ACTIVE_USER"; +const LABEL_LOG_SANITIZE_SENSITIVE_INFO: &str = "LABEL.SANITIZE_SENSITIVE_INFO"; const LOG_LEVELS: [&str; 5] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]; @@ -30,13 +38,10 @@ pub fn LogConfigView() -> Html { let config_ctx = use_context::().expect("ConfigContext not found"); let config_view_ctx = use_context::().expect("ConfigViewContext not found"); - let log_level_options = use_memo((), |_| { - LOG_LEVELS.iter().map(ToString::to_string).collect::>() - }); + let log_level_options = use_memo((), |_| LOG_LEVELS.iter().map(ToString::to_string).collect::>()); - let form_state: UseReducerHandle = use_reducer(|| { - LogConfigFormState { form: LogConfigDto::default(), modified: false } - }); + let form_state: UseReducerHandle = + use_reducer(|| LogConfigFormState { form: LogConfigDto::default(), modified: false }); { let on_form_change = config_view_ctx.on_form_change.clone(); @@ -48,10 +53,7 @@ pub fn LogConfigView() -> Html { { let form_state = form_state.clone(); - let log_config = config_ctx - .config - .as_ref() - .and_then(|c| c.config.log.clone()); // clone() Option + let log_config = config_ctx.config.as_ref().and_then(|c| c.config.log.clone()); // clone() Option use_effect_with((log_config, config_view_ctx.edit_mode.clone()), move |(log_cfg, _mode)| { if let Some(log) = log_cfg { @@ -87,7 +89,8 @@ pub fn LogConfigView() -> Html { let render_edit_mode = || { let forms = form_state.clone(); - let log_level_selection = Rc::new(forms.form.log_level.as_ref().map_or_else(Vec::new, |l| vec![l.to_uppercase()])); + let log_level_selection = + Rc::new(forms.form.log_level.as_ref().map_or_else(Vec::new, |l| vec![l.to_uppercase()])); html! { <> @@ -125,4 +128,4 @@ pub fn LogConfigView() -> Html { } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/config/macros.rs b/frontend/src/app/components/config/macros.rs index cf6d926b0..7cdbba4d8 100644 --- a/frontend/src/app/components/config/macros.rs +++ b/frontend/src/app/components/config/macros.rs @@ -140,10 +140,10 @@ pub trait HasFormData { #[macro_export] macro_rules! edit_field_text_option { ($instance:expr, $label:expr, $field:ident, $action:path) => { - $crate::edit_field_text_option!(@inner $instance, $label, $field, $action, false) +$crate::edit_field_text_option!(@inner $instance, $label, $field, $action, false) }; ($instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr) => { - $crate::edit_field_text_option!(@inner $instance, $label, $field, $action, $hidden) +$crate::edit_field_text_option!(@inner $instance, $label, $field, $action, $hidden) }; (@inner $instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr) => {{ let instance = $instance.clone(); @@ -194,10 +194,10 @@ macro_rules! edit_field_textarea_option { #[macro_export] macro_rules! edit_field_text { ($instance:expr, $label:expr, $field:ident, $action:path) => { - $crate::edit_field_text!(@inner $instance, $label, $field, $action, false) +$crate::edit_field_text!(@inner $instance, $label, $field, $action, false) }; ($instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr) => { - $crate::edit_field_text!(@inner $instance, $label, $field, $action, $hidden) +$crate::edit_field_text!(@inner $instance, $label, $field, $action, $hidden) }; (@inner $instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr) => {{ let instance = $instance.clone(); @@ -335,7 +335,6 @@ macro_rules! edit_field_number_u16 { }}; } - #[macro_export] macro_rules! edit_field_number_i16 { ($instance:expr, $label:expr, $field:ident, $action:path) => {{ @@ -509,9 +508,12 @@ macro_rules! edit_field_date { macro_rules! edit_field_list { ($instance:expr, $label:expr, $field:ident, $action:path, $placeholder:expr) => {{ let instance = $instance.clone(); - let tag_list = instance.form.$field.iter() - .map(|s| std::rc::Rc::new($crate::app::components::Tag { label: s.clone(), class: None })) - .collect::>>(); + let tag_list = instance + .form + .$field + .iter() + .map(|s| std::rc::Rc::new($crate::app::components::Tag { label: s.clone(), class: None })) + .collect::>>(); html! {
@@ -532,9 +534,11 @@ macro_rules! edit_field_list { macro_rules! edit_field_list_option { ($instance:expr, $label:expr, $field:ident, $action:path, $placeholder:expr) => {{ let instance = $instance.clone(); - let tag_list = instance.form.$field.as_ref().map_or_else(Vec::new, |f| f.iter() - .map(|s| std::rc::Rc::new($crate::app::components::Tag { label: s.clone(), class: None })) - .collect::>>()); + let tag_list = instance.form.$field.as_ref().map_or_else(Vec::new, |f| { + f.iter() + .map(|s| std::rc::Rc::new($crate::app::components::Tag { label: s.clone(), class: None })) + .collect::>>() + }); html! {
diff --git a/frontend/src/app/components/config/main_config_view.rs b/frontend/src/app/components/config/main_config_view.rs index 89b33d26c..ca46b7590 100644 --- a/frontend/src/app/components/config/main_config_view.rs +++ b/frontend/src/app/components/config/main_config_view.rs @@ -1,11 +1,17 @@ +use crate::{ + app::{ + components::config::{ + config_page::{ConfigForm, LABEL_MAIN_CONFIG}, + config_view_context::ConfigViewContext, + }, + context::ConfigContext, + }, + config_field, config_field_bool, config_field_optional, edit_field_bool, edit_field_number, + edit_field_number_option_u32, edit_field_text, edit_field_text_option, generate_form_reducer, +}; +use shared::model::MainConfigDto; use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::MainConfigDto; -use crate::app::context::ConfigContext; -use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::config::config_page::{ConfigForm, LABEL_MAIN_CONFIG}; -use crate::{config_field_optional, config_field_bool, config_field, edit_field_text_option, edit_field_bool, - generate_form_reducer, edit_field_number, edit_field_number_option_u32, edit_field_text}; const LABEL_UPDATE_ON_BOOT: &str = "LABEL.UPDATE_ON_BOOT"; const LABEL_CONFIG_HOT_RELOAD: &str = "LABEL.CONFIG_HOT_RELOAD"; @@ -49,9 +55,8 @@ pub fn MainConfigView() -> Html { let config_ctx = use_context::().expect("ConfigContext not found"); let config_view_ctx = use_context::().expect("ConfigViewContext not found"); - let form_state: UseReducerHandle = use_reducer(|| { - MainConfigFormState { form: MainConfigDto::default(), modified: false } - }); + let form_state: UseReducerHandle = + use_reducer(|| MainConfigFormState { form: MainConfigDto::default(), modified: false }); { let on_form_change = config_view_ctx.on_form_change.clone(); @@ -66,7 +71,7 @@ pub fn MainConfigView() -> Html { let config = config_ctx.config.as_ref().map(|c| c.config.clone()); use_effect_with((config, config_view_ctx.edit_mode.clone()), move |(cfg, _mode)| { if let Some(main) = cfg { - let main_config = MainConfigDto::from(main); + let main_config = MainConfigDto::from(main); form_state.dispatch(MainConfigFormAction::SetAll(main_config.clone())); } else { form_state.dispatch(MainConfigFormAction::SetAll(MainConfigDto::default())); @@ -96,23 +101,25 @@ pub fn MainConfigView() -> Html { } }; - let render_edit_mode = || html! { - <> - { edit_field_bool!(form_state, translate.t(LABEL_UPDATE_ON_BOOT), update_on_boot, MainConfigFormAction::UpdateOnBoot) } - { edit_field_bool!(form_state, translate.t(LABEL_CONFIG_HOT_RELOAD), config_hot_reload, MainConfigFormAction::ConfigHotReload) } - { edit_field_bool!(form_state, translate.t(LABEL_USER_ACCESS_CONTROL), user_access_control, MainConfigFormAction::UserAccessControl) } - { edit_field_bool!(form_state, translate.t(LABEL_ACCEPT_INSECURE_SSL_CERTIFICATES), accept_insecure_ssl_certificates, MainConfigFormAction::AcceptInsecureSslCertificates) } - { edit_field_bool!(form_state, translate.t(LABEL_PROCESS_PARALLEL), process_parallel, MainConfigFormAction::ProcessParallel) } - { edit_field_bool!(form_state, translate.t(LABEL_DISK_BASED_PROCESSING), disk_based_processing, MainConfigFormAction::DiskBasedProcessing) } - { edit_field_text!(form_state, translate.t(LABEL_WORKING_DIR), working_dir, MainConfigFormAction::WorkingDir) } - { edit_field_text_option!(form_state, translate.t(LABEL_DEFAULT_USER_AGENT), default_user_agent, MainConfigFormAction::DefaultUserAgent) } - { edit_field_text_option!(form_state, translate.t(LABEL_MAPPING_PATH), mapping_path, MainConfigFormAction::MappingPath) } - { edit_field_text_option!(form_state, translate.t(LABEL_BACKUP_DIR), backup_dir, MainConfigFormAction::BackupDir) } - { edit_field_text_option!(form_state, translate.t(LABEL_USER_CONFIG_DIR), user_config_dir, MainConfigFormAction::UserConfigDir) } - { edit_field_number_option_u32!(form_state, translate.t(LABEL_SLEEP_TIMER_MINS), sleep_timer_mins, MainConfigFormAction::SleepTimerMins) } - { edit_field_number!(form_state, translate.t(LABEL_CONNECT_TIMEOUT_SECS), connect_timeout_secs, MainConfigFormAction::ConnectTimeoutSecs) } - { edit_field_text_option!(form_state, translate.t(LABEL_CUSTOM_STREAM_RESPONSE_PATH), custom_stream_response_path, MainConfigFormAction::CustomStreamResponsePath) } - + let render_edit_mode = || { + html! { + <> + { edit_field_bool!(form_state, translate.t(LABEL_UPDATE_ON_BOOT), update_on_boot, MainConfigFormAction::UpdateOnBoot) } + { edit_field_bool!(form_state, translate.t(LABEL_CONFIG_HOT_RELOAD), config_hot_reload, MainConfigFormAction::ConfigHotReload) } + { edit_field_bool!(form_state, translate.t(LABEL_USER_ACCESS_CONTROL), user_access_control, MainConfigFormAction::UserAccessControl) } + { edit_field_bool!(form_state, translate.t(LABEL_ACCEPT_INSECURE_SSL_CERTIFICATES), accept_insecure_ssl_certificates, MainConfigFormAction::AcceptInsecureSslCertificates) } + { edit_field_bool!(form_state, translate.t(LABEL_PROCESS_PARALLEL), process_parallel, MainConfigFormAction::ProcessParallel) } + { edit_field_bool!(form_state, translate.t(LABEL_DISK_BASED_PROCESSING), disk_based_processing, MainConfigFormAction::DiskBasedProcessing) } + { edit_field_text!(form_state, translate.t(LABEL_WORKING_DIR), working_dir, MainConfigFormAction::WorkingDir) } + { edit_field_text_option!(form_state, translate.t(LABEL_DEFAULT_USER_AGENT), default_user_agent, MainConfigFormAction::DefaultUserAgent) } + { edit_field_text_option!(form_state, translate.t(LABEL_MAPPING_PATH), mapping_path, MainConfigFormAction::MappingPath) } + { edit_field_text_option!(form_state, translate.t(LABEL_BACKUP_DIR), backup_dir, MainConfigFormAction::BackupDir) } + { edit_field_text_option!(form_state, translate.t(LABEL_USER_CONFIG_DIR), user_config_dir, MainConfigFormAction::UserConfigDir) } + { edit_field_number_option_u32!(form_state, translate.t(LABEL_SLEEP_TIMER_MINS), sleep_timer_mins, MainConfigFormAction::SleepTimerMins) } + { edit_field_number!(form_state, translate.t(LABEL_CONNECT_TIMEOUT_SECS), connect_timeout_secs, MainConfigFormAction::ConnectTimeoutSecs) } + { edit_field_text_option!(form_state, translate.t(LABEL_CUSTOM_STREAM_RESPONSE_PATH), custom_stream_response_path, MainConfigFormAction::CustomStreamResponsePath) } + + } }; html! { diff --git a/frontend/src/app/components/config/messaging_config_view.rs b/frontend/src/app/components/config/messaging_config_view.rs index 0f7d63027..06d1b0ec8 100644 --- a/frontend/src/app/components/config/messaging_config_view.rs +++ b/frontend/src/app/components/config/messaging_config_view.rs @@ -1,12 +1,23 @@ -use crate::app::components::config::config_page::{ConfigForm, LABEL_MESSAGING_CONFIG}; -use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::{Card, Chip, RadioButtonGroup, TextArea}; -use crate::app::ConfigContext; -use crate::{config_field, config_field_bool, config_field_bool_empty, config_field_child, config_field_custom, config_field_empty, config_field_hide, config_field_optional, edit_field_bool, edit_field_list, edit_field_text, edit_field_text_option, generate_form_reducer}; -use shared::model::{DiscordMessagingConfigDto, MessagingConfigDto, MsgKind, PushoverMessagingConfigDto, - RestMessagingConfigDto, TelegramMessagingConfigDto}; -use std::rc::Rc; -use std::str::FromStr; +use crate::{ + app::{ + components::{ + config::{ + config_page::{ConfigForm, LABEL_MESSAGING_CONFIG}, + config_view_context::ConfigViewContext, + }, + Card, Chip, RadioButtonGroup, TextArea, + }, + ConfigContext, + }, + config_field, config_field_bool, config_field_bool_empty, config_field_child, config_field_custom, + config_field_empty, config_field_hide, config_field_optional, edit_field_bool, edit_field_list, edit_field_text, + edit_field_text_option, generate_form_reducer, +}; +use shared::model::{ + DiscordMessagingConfigDto, MessagingConfigDto, MsgKind, PushoverMessagingConfigDto, RestMessagingConfigDto, + TelegramMessagingConfigDto, +}; +use std::{rc::Rc, str::FromStr}; use yew::prelude::*; use yew_i18n::use_translation; @@ -81,43 +92,25 @@ pub fn MessagingConfigView() -> Html { let config_ctx = use_context::().expect("ConfigContext not found"); let config_view_ctx = use_context::().expect("ConfigViewContext not found"); - let telegram_state = - use_reducer(|| TelegramMessagingConfigFormState { - form: TelegramMessagingConfigDto::default(), - modified: false, - }); + let telegram_state = use_reducer(|| TelegramMessagingConfigFormState { + form: TelegramMessagingConfigDto::default(), + modified: false, + }); let rest_state = - use_reducer(|| RestMessagingConfigFormState { - form: RestMessagingConfigDto::default(), - modified: false, - }); + use_reducer(|| RestMessagingConfigFormState { form: RestMessagingConfigDto::default(), modified: false }); - let pushover_state = - use_reducer(|| PushoverMessagingConfigFormState { - form: PushoverMessagingConfigDto::default(), - modified: false, - }); + let pushover_state = use_reducer(|| PushoverMessagingConfigFormState { + form: PushoverMessagingConfigDto::default(), + modified: false, + }); let discord_state = - use_reducer(|| DiscordMessagingConfigFormState { - form: DiscordMessagingConfigDto::default(), - modified: false, - }); + use_reducer(|| DiscordMessagingConfigFormState { form: DiscordMessagingConfigDto::default(), modified: false }); let messaging_state = - use_reducer(|| MessagingConfigFormState { - form: MessagingConfigDto::default(), - modified: false, - }); + use_reducer(|| MessagingConfigFormState { form: MessagingConfigDto::default(), modified: false }); - let notify_on_options = use_memo((), |_| { - vec![ - MsgKind::Info, - MsgKind::Stats, - MsgKind::Error, - MsgKind::Watch, - ] - }); + let notify_on_options = use_memo((), |_| vec![MsgKind::Info, MsgKind::Stats, MsgKind::Error, MsgKind::Watch]); let notify_on_options_text = use_memo((*notify_on_options).clone(), |options: &Vec| { options.iter().map(ToString::to_string).collect::>() @@ -155,7 +148,6 @@ pub fn MessagingConfigView() -> Html { }); } - { let msg_state = messaging_state.clone(); let t_state = telegram_state.clone(); @@ -342,14 +334,7 @@ pub fn MessagingConfigView() -> Html { let render_edit_mode = || { let msg_state = messaging_state.clone(); - let notify_on_selections = Rc::new( - msg_state - .form - .notify_on - .iter() - .map(ToString::to_string) - .collect(), - ); + let notify_on_selections = Rc::new(msg_state.form.notify_on.iter().map(ToString::to_string).collect()); html! { <>
@@ -379,7 +364,7 @@ pub fn MessagingConfigView() -> Html { let telegram_state = telegram_state.clone(); let kind = *kind; html! { -