diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6acbb61..0b477ce 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -11,8 +11,6 @@ name: Check env: CARGO_TERM_COLOR: always -# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel -# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true @@ -24,14 +22,18 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: components: rustfmt + - name: Run cargo fmt run: cargo fmt -- --check + - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 + clippy: name: ${{ matrix.toolchain }} / clippy runs-on: ubuntu-latest @@ -45,12 +47,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: components: clippy + - name: Run clippy action uses: clechasseur/rs-clippy-check@v3 + - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 doc: @@ -61,8 +66,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install Rust nightly uses: dtolnay/rust-toolchain@nightly + - name: Run cargo doc run: cargo doc --no-deps --all-features env: @@ -73,9 +80,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install 1.82.0 uses: dtolnay/rust-toolchain@master with: toolchain: 1.82.0 + - name: cargo +1.82.0 check run: cargo check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82af23a..1537cf1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,8 +18,152 @@ on: - major jobs: - bump: + create-artifacts-directory: runs-on: ubuntu-latest + steps: + - name: Check if actor is repository owner + if: ${{ github.actor != github.repository_owner }} + run: | + echo "You are not authorized to run this workflow." + exit 1 + + - name: Create artifacts directory + run: mkdir artifacts + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: artifacts + path: artifacts + + build-release-artifacts: + name: build-release + needs: [create-artifacts-directory] + runs-on: ${{ matrix.job.os }} + env: + RUST_BACKTRACE: 1 + strategy: + fail-fast: true + matrix: + # prettier-ignore + job: + - { name: "macOS-arm64", os: "macOS-latest", target: "aarch64-apple-darwin", artifact_suffix: "macos-arm64", use-cross: true } + - { name: "macOS-amd64", os: "macOS-latest", target: "x86_64-apple-darwin", artifact_suffix: "macos" } + - { name: "windows-amd64", os: "windows-latest", target: "x86_64-pc-windows-msvc", artifact_suffix: "windows" } + - { name: "windows-aarch64", os: "windows-latest", target: "aarch64-pc-windows-msvc", artifact_suffix: "windows-aarch64", use-cross: true } + - { name: "linux-gnu", os: "ubuntu-latest", target: "x86_64-unknown-linux-gnu", artifact_suffix: "linux" } + - { name: "linux-musl", os: "ubuntu-latest", target: "x86_64-unknown-linux-musl", artifact_suffix: "linux-musl", use-cross: true, } + - { name: "linux-aarch64-gnu", os: "ubuntu-latest", target: "aarch64-unknown-linux-gnu", artifact_suffix: "aarch64-gnu", use-cross: true, test-bin: "--bin managarr" } + - { name: "linux-aarch64-musl", os: "ubuntu-latest", target: "aarch64-unknown-linux-musl", artifact_suffix: "aarch64-musl", use-cross: true, test-bin: "--bin managarr" } + - { name: "linux-arm-gnu", os: "ubuntu-latest", target: "arm-unknown-linux-gnueabi", artifact_suffix: "armv6-gnu", use-cross: true, test-bin: "--bin managarr" } + - { name: "linux-arm-musl", os: "ubuntu-latest", target: "arm-unknown-linux-musleabihf", artifact_suffix: "armv6-musl", use-cross: true, test-bin: "--bin managarr" } + - { name: "linux-armv7-gnu", os: "ubuntu-latest", target: "armv7-unknown-linux-gnueabihf", artifact_suffix: "armv7-gnu", use-cross: true, test-bin: "--bin managarr" } + - { name: "linux-armv7-musl", os: "ubuntu-latest", target: "armv7-unknown-linux-musleabihf", artifact_suffix: "armv7-musl", use-cross: true, test-bin: "--bin managarr" } + rust: [stable] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: actions/cache@v3 + name: Cache Cargo registry + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} + + - uses: actions/cache@v3 + if: startsWith(matrix.job.name, 'linux-') + with: + path: ~/.cargo/bin + key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }} + + - uses: dtolnay/rust-toolchain@stable + name: Set Rust toolchain + with: + targets: ${{ matrix.job.target }} + + - uses: taiki-e/setup-cross-toolchain-action@v1 + with: + # NB: sets CARGO_BUILD_TARGET evar - do not need --target flag in build + target: ${{ matrix.job.target }} + + - uses: taiki-e/install-action@cross + if: ${{ matrix.job.use-cross }} + + - name: Installing needed Ubuntu dependencies + if: matrix.job.os == 'ubuntu-latest' + shell: bash + run: | + sudo apt-get -y update + case ${{ matrix.job.target }} in + arm*-linux-*) sudo apt-get -y install gcc-arm-linux-gnueabihf ;; + aarch64-*-linux-*) sudo apt-get -y install gcc-aarch64-linux-gnu ;; + esac + + - name: Build + run: cargo build --release --verbose --target=${{ matrix.job.target }} --locked + + - name: Verify file + shell: bash + run: | + file target/${{ matrix.job.target }}/release/managarr + + - name: Test + if: matrix.job.target != 'aarch64-apple-darwin' && matrix.job.target != 'aarch64-pc-windows-msvc' + run: cargo test --release --verbose --target=${{ matrix.job.target }} ${{ matrix.job.test-bin }} + + - name: Packaging final binary (Windows) + if: matrix.job.os == 'windows-latest' + shell: bash + run: | + cd target/${{ matrix.job.target }}/release + BINARY_NAME=managarr.exe + if [ "${{ matrix.job.target }}" != "aarch64-pc-windows-msvc" ]; then + # strip the binary + strip $BINARY_NAME + fi + RELEASE_NAME=managarr-${{ matrix.job.artifact_suffix }} + tar czvf $RELEASE_NAME.tar.gz $BINARY_NAME + # create sha checksum files + certutil -hashfile $RELEASE_NAME.tar.gz sha256 | grep -E [A-Fa-f0-9]{64} > $RELEASE_NAME.sha256 + echo "RELEASE_NAME=$RELEASE_NAME" >> $GITHUB_ENV + + - name: Packaging final binary (macOS and Linux) + if: matrix.job.os != 'windows-latest' + shell: bash + run: | + # set the right strip executable + STRIP="strip"; + case ${{ matrix.job.target }} in + arm*-linux-*) STRIP="arm-linux-gnueabihf-strip" ;; + aarch64-*-linux-*) STRIP="aarch64-linux-gnu-strip" ;; + esac; + cd target/${{ matrix.job.target }}/release + BINARY_NAME=managarr + # strip the binary + "$STRIP" "$BINARY_NAME" + RELEASE_NAME=managarr-${{ matrix.job.artifact_suffix }} + tar czvf $RELEASE_NAME.tar.gz $BINARY_NAME + # create sha checksum files + shasum -a 256 $RELEASE_NAME.tar.gz > $RELEASE_NAME.sha256 + echo "RELEASE_NAME=$RELEASE_NAME" >> $GITHUB_ENV + + - name: Add SHA to artifacts + run: | + cp target/${{ matrix.job.target }}/release/${{ env.RELEASE_NAME }}.sha256 artifacts/ + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: artifacts + path: artifacts + + publish-github-release: + name: publish-github-release + needs: [build-release-artifacts] + runs-on: ${{ matrix.job.os }} steps: - name: Configure SSH for Git run: | @@ -34,6 +178,12 @@ jobs: ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }} fetch-depth: 0 + - name: Download all artifacts + uses: actions/download-artifact@v4.1.7 + with: + name: artifacts + path: artifacts + - name: Set up Python uses: actions/setup-python@v4 with: @@ -73,6 +223,7 @@ jobs: NEW_TAG=$(cz version --project) echo "New version: $NEW_TAG" echo "version=$NEW_TAG" >> $GITHUB_ENV + echo "$NEW_TAG" > artifacts/release-version - name: Get the previous version tag id: prev_version @@ -93,6 +244,31 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: + files: | + target/aarch64-apple-darwin/managarr-macos-arm64.tar.gz + target/aarch64-apple-darwin/managarr-macos-arm64.sha256 + target/x86_64-apple-darwin/managarr-macos.tar.gz + target/x86_64-apple-darwin/managarr-macos.sha256 + target/x86_64-pc-windows-msvc/managarr-windows.tar.gz + target/x86_64-pc-windows-msvc/managarr-windows.sha256 + target/aarch64-pc-windows-msvc/managarr-windows-aarch64.tar.gz + target/aarch64-pc-windows-msvc/managarr-windows-aarch64.sha256 + target/x86_64-unknown-linux-gnu/managarr-linux.tar.gz + target/x86_64-unknown-linux-gnu/managarr-linux.sha256 + target/x86_64-unknown-linux-musl/managarr-linux-musl.tar.gz + target/x86_64-unknown-linux-musl/managarr-linux-musl.sha256 + target/aarch64-unknown-linux-gnu/managarr-aarch64-gnu.tar.gz + target/aarch64-unknown-linux-gnu/managarr-aarch64-gnu.sha256 + target/aarch64-unknown-linux-musl/managarr-aarch64-musl.tar.gz + target/aarch64-unknown-linux-musl/managarr-aarch64-musl.sha256 + target/arm-unknown-linux-gnueabi/managarr-armv6-gnu.tar.gz + target/arm-unknown-linux-gnueabi/managarr-armv6-gnu.sha256 + target/arm-unknown-linux-musleabihf/managarr-armv6-musl.tar.gz + target/arm-unknown-linux-musleabihf/managarr-armv6-musl.sha256 + target/armv7-unknown-linux-gnueabihf/managarr-armv7-gnu.tar.gz + target/armv7-unknown-linux-gnueabihf/managarr-armv7-gnu.sha256 + target/armv7-unknown-linux-musleabihf/managarr-armv7-musl.tar.gz + target/armv7-unknown-linux-musleabihf/managarr-armv7-musl.sha256 tag_name: v${{ env.version }} name: "v${{ env.version }}" body: ${{ env.changelog_body }} @@ -105,9 +281,48 @@ jobs: run: | git push origin --follow-tags - release-crate: - needs: bump - name: Release Crate + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: artifacts + path: artifacts + + publish-docker-image: + needs: [build-release-artifacts] + name: Publishing Docker image to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Get release artifacts + uses: actions/download-artifact@v4.1.7 + with: + name: artifacts + path: artifacts + + - name: Set release version + shell: bash + run: | + release_version="$(cat ./artifacts/release-version)" + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV + + - name: Validate release environment variables + run: | + echo "Release version: ${{ env.version }}" + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Push to Docker Hub + uses: docker/build-push-action@v5 + with: + tags: darkalex17/managarr:latest, darkalex17/managarr:${{ env.version }} + push: true + + publish-crate: + needs: publish-github-release + name: Publish Crate runs-on: ubuntu-latest steps: - name: Check if actor is repository owner @@ -126,6 +341,17 @@ jobs: git fetch --all git pull + - uses: actions/cache@v3 + name: Cache Cargo registry + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} + + - uses: actions/cache@v3 + with: + path: ~/.cargo/bin + key: ${{ runner.os }}-cargo-bin-${{ hashFiles('.github/workflows/release.yml') }} + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 386b2de..c7e4e28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,16 +34,20 @@ jobs: toolchain: [stable, beta] steps: - uses: actions/checkout@v4 + - name: Install ${{ matrix.toolchain }} uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} + # enable this ci template to run regardless of whether the lockfile is checked in or not - name: cargo generate-lockfile if: hashFiles('Cargo.lock') == '' run: cargo generate-lockfile + - name: cargo test --locked run: cargo test --locked --all-features --all-targets + minimal-versions: # This action chooses the oldest version of the dependencies permitted by Cargo.toml to ensure # that this crate is compatible with the minimal version that this crate and its dependencies @@ -71,18 +75,25 @@ jobs: name: ubuntu / stable / minimal-versions steps: - uses: actions/checkout@v4 + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable + - name: Install nightly for -Zdirect-minimal-versions uses: dtolnay/rust-toolchain@nightly + - name: rustup default stable run: rustup default stable + - name: cargo update -Zdirect-minimal-versions run: cargo +nightly update -Zdirect-minimal-versions + - name: cargo test run: cargo test --locked --all-features --all-targets + - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 + os-check: # run cargo test on mac and windows runs-on: ${{ matrix.os }} @@ -100,15 +111,20 @@ jobs: # if: runner.os == 'Windows' - name: Checkout uses: actions/checkout@v4 + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable + - name: cargo generate-lockfile if: hashFiles('Cargo.lock') == '' run: cargo generate-lockfile + - name: cargo test run: cargo test --locked --all-features --all-targets + - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 + coverage: # use llvm-cov to build and collect coverage and outputs in a format that # is compatible with codecov.io @@ -136,21 +152,28 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview + - name: cargo install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov + - name: cargo generate-lockfile if: hashFiles('Cargo.lock') == '' run: cargo generate-lockfile + - name: cargo llvm-cov run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info + - name: Record Rust version run: echo "RUST=$(rustc --version)" >> "$GITHUB_ENV" + - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 + - name: Upload to codecov.io uses: codecov/codecov-action@v4 with: diff --git a/Cargo.lock b/Cargo.lock index 626e5e3..94bcfc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -98,9 +98,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "arc-swap" @@ -142,7 +142,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -195,9 +195,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "regex-automata", @@ -218,15 +218,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cargo-husky" -version = "1.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad" +checksum = "fa108bb6da8de0669ab0fef3a4afabcc3446938b09b1ffe2e90486c75df8f215" [[package]] name = "cassowary" @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "shlex", ] @@ -281,9 +281,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -291,9 +291,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.38" +version = "4.5.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" +checksum = "9646e2e245bf62f45d39a0f3f36f1171ad1ea0d6967fd114bca72cb02a8fcdfb" dependencies = [ "clap", ] @@ -319,14 +319,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" @@ -360,13 +360,13 @@ dependencies = [ [[package]] name = "confy" -version = "0.6.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45b1f4c00870f07dc34adcac82bb6a72cc5aabca8536ba1797e01df51d2ce9a0" +checksum = "15d296c475c6ed4093824c28e222420831d27577aaaf0a1163a3b7fc35b248a5" dependencies = [ "directories", "serde", - "serde_yaml", + "serde_yaml 0.9.16", "thiserror", ] @@ -407,7 +407,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags", "crossterm_winapi", - "mio", + "mio 1.0.3", "parking_lot", "rustix", "signal-hook", @@ -424,6 +424,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "ctrlc" version = "3.4.5" @@ -455,7 +465,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -466,7 +476,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -489,12 +499,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_setters" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8ef033054e131169b8f0f9a7af8f5533a9436fadf3c500ed547f730f07090d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "destructure_traitobject" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + [[package]] name = "diff" version = "0.1.13" @@ -557,7 +585,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -601,19 +629,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fnv" @@ -713,7 +741,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -775,6 +803,25 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.7.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.7" @@ -786,14 +833,20 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", - "indexmap", + "http 1.2.0", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.2" @@ -819,15 +872,37 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "http" -version = "1.1.0" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", "itoa", ] +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -835,7 +910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.2.0", ] [[package]] @@ -846,8 +921,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http", - "http-body", + "http 1.2.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -885,6 +960,30 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.5.1" @@ -894,11 +993,10 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", "httparse", - "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -913,8 +1011,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http", - "hyper", + "http 1.2.0", + "hyper 1.5.1", "hyper-util", "rustls", "rustls-pki-types", @@ -931,7 +1029,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.5.1", "hyper-util", "native-tls", "tokio", @@ -948,9 +1046,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.2.0", + "http-body 1.0.1", + "hyper 1.5.1", "pin-project-lite", "socket2", "tokio", @@ -1096,7 +1194,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1128,12 +1226,22 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1151,22 +1259,18 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.5" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +checksum = "6fe2b9d82064e8a0226fddb3547f37f28eaa46d0fc210e275d835f08cf3b76a7" [[package]] name = "instability" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" dependencies = [ - "darling", - "indoc", - "pretty_assertions", - "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1198,10 +1302,11 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1213,9 +1318,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.165" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb4d3d38eab6c5239a362fa8bae48c03baf980a6e7079f063942d563ef3533e" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libredox" @@ -1227,6 +1332,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1251,10 +1362,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ + "cfg-if", "serde", ] @@ -1266,9 +1378,9 @@ checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" [[package]] name = "log4rs" -version = "1.3.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" +checksum = "d36ca1786d9e79b8193a68d480a0907b612f109537115c6ff655a3a1967533fd" dependencies = [ "anyhow", "arc-swap", @@ -1279,13 +1391,11 @@ dependencies = [ "libc", "log", "log-mdc", - "once_cell", "parking_lot", - "rand", "serde", "serde-value", "serde_json", - "serde_yaml", + "serde_yaml 0.8.26", "thiserror", "thread-id", "typemap-ors", @@ -1298,7 +1408,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1319,6 +1429,8 @@ dependencies = [ "crossterm", "ctrlc", "derivative", + "derive_setters", + "deunicode", "dirs-next", "human-panic", "indicatif", @@ -1329,6 +1441,7 @@ dependencies = [ "managarr-tree-widget", "mockall", "mockito", + "paste", "pretty_assertions", "ratatui", "regex", @@ -1336,7 +1449,7 @@ dependencies = [ "rstest", "serde", "serde_json", - "serde_yaml", + "serde_yaml 0.9.16", "strum", "strum_macros", "tokio", @@ -1377,11 +1490,21 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "log", "wasi", @@ -1390,9 +1513,9 @@ dependencies = [ [[package]] name = "mockall" -version = "0.13.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" dependencies = [ "cfg-if", "downcast", @@ -1404,31 +1527,27 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.13.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "mockito" -version = "1.6.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652cd6d169a36eaf9d1e6bce1a221130439a966d7f27858af66a33a66e9c4ee2" +checksum = "8c1eecc3baf782e3c8d6803cc8780268da1f32df6eb88c016c1d80b0df7944cf" dependencies = [ "assert-json-diff", - "bytes", "colored", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", + "futures", + "hyper 0.14.31", + "lazy_static", "log", "rand", "regex", @@ -1482,6 +1601,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -1535,7 +1664,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1573,15 +1702,24 @@ dependencies = [ [[package]] name = "os_info" -version = "3.8.2" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +checksum = "e5ca711d8b83edbb00b44d504503cd247c9c0bd8b0fa2694f2a1a3d8165379ce" dependencies = [ "log", "serde", "windows-sys 0.52.0", ] +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1685,11 +1823,13 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.4.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" dependencies = [ + "ctor", "diff", + "output_vt100", "yansi", ] @@ -1774,9 +1914,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags", ] @@ -1838,11 +1978,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.5.1", "hyper-rustls", "hyper-tls", "hyper-util", @@ -1911,7 +2051,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.89", + "syn 2.0.90", "unicode-ident", ] @@ -1932,22 +2072,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.18" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "rustls-pki-types", @@ -2034,15 +2174,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -2059,23 +2199,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ "itoa", - "memchr", "ryu", "serde", ] @@ -2103,11 +2242,23 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.34+deprecated" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ - "indexmap", + "indexmap 1.9.3", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "serde_yaml" +version = "0.9.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92b5b431e8907b50339b51223b97d102db8d987ced36f6e4d03621db9316c834" +dependencies = [ + "indexmap 1.9.3", "itoa", "ryu", "serde", @@ -2137,7 +2288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 1.0.3", "signal-hook", ] @@ -2173,9 +2324,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2224,7 +2375,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -2246,9 +2397,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.89" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -2272,7 +2423,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -2332,7 +2483,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -2347,9 +2498,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "libc", @@ -2378,31 +2529,32 @@ dependencies = [ [[package]] name = "tokio" -version = "1.41.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 0.8.11", + "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -2417,26 +2569,26 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", + "tracing", ] [[package]] @@ -2466,7 +2618,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", @@ -2481,9 +2633,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-core", @@ -2582,9 +2734,9 @@ dependencies = [ [[package]] name = "urlencoding" -version = "2.1.3" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" [[package]] name = "utf16_iter" @@ -2645,9 +2797,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -2656,36 +2808,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2693,28 +2845,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -2961,10 +3113,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] -name = "yansi" -version = "1.0.1" +name = "yaml-rust" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "yoke" @@ -2986,7 +3147,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "synstructure", ] @@ -3008,7 +3169,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -3028,7 +3189,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "synstructure", ] @@ -3057,5 +3218,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index 7886fd5..82e13d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,10 @@ strum = { version = "0.26.3", features = ["derive"] } strum_macros = "0.26.4" tokio = { version = "1.36.0", features = ["full"] } tokio-util = "0.7.8" -ratatui = { version = "0.29.0", features = ["all-widgets"] } +ratatui = { version = "0.29.0", features = [ + "all-widgets", + "unstable-widget-ref", +] } urlencoding = "2.1.2" clap = { version = "4.5.20", features = ["derive", "cargo", "env"] } clap_complete = "4.5.33" @@ -47,6 +50,9 @@ async-trait = "0.1.83" dirs-next = "2.0.0" managarr-tree-widget = "0.24.0" indicatif = "0.17.9" +derive_setters = "0.1.6" +deunicode = "1.6.0" +paste = "1.0.15" [dev-dependencies] assert_cmd = "2.0.16" diff --git a/README.md b/README.md index 145512e..4e9b064 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,16 @@ ![License](https://img.shields.io/badge/license-MIT-blueviolet.svg) ![LOC](https://tokei.rs/b1/github/Dark-Alex-17/managarr?category=code) [![crates.io link](https://img.shields.io/crates/v/managarr.svg)](https://crates.io/crates/managarr) +![Docker Release](https://img.shields.io/docker/v/darkalex17/managarr?label=Docker%20version) ![Release](https://img.shields.io/github/v/release/Dark-Alex-17/managarr?color=%23c694ff) [![codecov](https://codecov.io/gh/Dark-Alex-17/managarr/graph/badge.svg?token=33G179TW67)](https://codecov.io/gh/Dark-Alex-17/managarr) ![Crate.io downloads](https://img.shields.io/crates/d/managarr?label=Crate%20downloads) +[![GitHub Downloads](https://img.shields.io/github/downloads/Dark-Alex-17/managarr/total.svg?label=GitHub%20downloads)](https://github.com/Dark-Alex-17/managarr/releases) +![Docker pulls](https://img.shields.io/docker/pulls/darkalex17/managarr?label=Docker%20downloads) Managarr is a TUI and CLI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust! -![library](screenshots/library.png) +![library](screenshots/sonarr/sonarr_library.png) ## What Servarrs are supported? @@ -53,6 +56,30 @@ You can also clone this repo and run `make docker` to build a docker image local Please note that you will need to create and popular your configuration file first before starting the container. Otherwise, the container will fail to start. +### Manual +Binaries are available on the [releases](https://github.com/Dark-Alex-17/managarr/releases) page for the following platforms: + +| Platform | Architecture(s) | +|----------------|----------------------------| +| macOS | x86_64, arm64 | +| Linux GNU/MUSL | x86_64,armv6,armv7,aarch64 | +| Windows | x86_64,aarch64 | + +#### Windows Instructions +To use a binary from the releases page on Windows, do the following: + +1. Download the latest binary [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS. +2. Use 7-Zip or TarTool to unpack the Tar file. +3. Run the executable `managarr.exe`! + +#### Linux/MacOS Instructions +To use a binary from the releases page on Linux/MacOS, do the following: + +1. Download the latest binary [binary](https://github.com/Dark-Alex-17/managarr/releases) for your OS. +2. `cd` to the directory where you downloaded the binary. +3. Extract the binary with `tar -C /usr/local/bin -xzf managarr-.tar.gz` (NB: This may require `sudo`) +4. Now you can run `managarr`! + ## Features Key: @@ -88,22 +115,21 @@ Key: | TUI | CLI | Feature | |-----|-----|--------------------------------------------------------------------------------------------------------------------| -| 🕒 | ✅ | View your library, downloads, blocklist, episodes | -| 🕒 | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits | -| 🕒 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings | -| 🕒 | ✅ | Search your library | -| 🕒 | ✅ | Add series to your library | -| 🕒 | ✅ | Delete series, downloads, indexers, root folders, and episode files | -| 🕒 | ✅ | Mark history events as failed | -| 🕒 | ✅ | Trigger automatic searches for series, seasons, or episodes | -| 🕒 | ✅ | Trigger refresh and disk scan for series and downloads | -| 🕒 | ✅ | Manually search for series, seasons, or episodes | -| 🕒 | ✅ | Edit your series and indexers | -| 🕒 | ✅ | Manage your tags | -| 🕒 | ✅ | Manage your root folders | -| 🕒 | ✅ | Manage your blocklist | -| 🕒 | ✅ | View and browse logs, tasks, events queues, and updates | -| 🕒 | ✅ | Manually trigger scheduled tasks | +| ✅ | ✅ | View your library, downloads, blocklist, episodes | +| ✅ | ✅ | View details of a specific series, or episode including description, history, downloaded file info, or the credits | +| 🚫 | ✅ | View your host and security configs from the CLI to programmatically fetch the API token, among other settings | +| ✅ | ✅ | Search your library | +| ✅ | ✅ | Add series to your library | +| ✅ | ✅ | Delete series, downloads, indexers, root folders, and episode files | +| ✅ | ✅ | Trigger automatic searches for series, seasons, or episodes | +| ✅ | ✅ | Trigger refresh and disk scan for series and downloads | +| ✅ | ✅ | Manually search for series, seasons, or episodes | +| ✅ | ✅ | Edit your series and indexers | +| ✅ | ✅ | Manage your tags | +| ✅ | ✅ | Manage your root folders | +| ✅ | ✅ | Manage your blocklist | +| ✅ | ✅ | View and browse logs, tasks, events queues, and updates | +| ✅ | ✅ | Manually trigger scheduled tasks | ### Readarr @@ -141,7 +167,7 @@ To see all available commands, simply run `managarr --help`: ```shell $ managarr --help -managarr 0.3.0 +managarr 0.4.0 Alex Clarke A TUI and CLI to manage your Servarrs @@ -186,6 +212,8 @@ Commands: start-task Start the specified Sonarr task test-indexer Test the indexer with the given ID. Note that a successful test returns an empty JSON body; i.e. '{}' test-all-indexers Test all Sonarr indexers + toggle-episode-monitoring Toggle monitoring for the specified episode + toggle-season-monitoring Toggle monitoring for the specified season that corresponds to the specified series ID help Print this message or the help of the given subcommand(s) Options: @@ -282,13 +310,22 @@ with all items tagged `Beta`. ## Screenshots -![library](screenshots/library.png) -![manual_search](screenshots/manual_search.png) -![logs](screenshots/logs.png) -![new_movie_search](screenshots/new_movie_search.png) -![add_new_movie](screenshots/add_new_movie.png) -![collection_details](screenshots/collection_details.png) -![indexers](screenshots/indexers.png) +### Radarr +![radarr_library](screenshots/radarr/radarr_library.png) +![manual_search](screenshots/radarr/manual_search.png) +![new_movie_search](screenshots/radarr/new_movie_search.png) +![add_new_movie](screenshots/radarr/add_new_movie.png) +![collection_details](screenshots/radarr/collection_details.png) + +### Sonarr +![sonarr_library](screenshots/sonarr/sonarr_library.png) +![series_details](screenshots/sonarr/series_details.png) +![season_details](screenshots/sonarr/season_details.png) +![manual_episode_search](screenshots/sonarr/manual_episode_search.png) + +### General +![logs](screenshots/radarr/logs.png) +![indexers](screenshots/radarr/indexers.png) ## Dependencies * [ratatui](https://github.com/tui-rs-revival/ratatui) @@ -300,7 +337,7 @@ with all items tagged `Beta`. ## Servarr Requirements * [Radarr >= 5.3.6.8612](https://radarr.video/docs/api/) -* [Sonarr >= v3](https://sonarr.tv/docs/api/) +* [Sonarr >= v4](https://sonarr.tv/docs/api/) * [Readarr v1](https://readarr.com/docs/api/) * [Lidarr v1](https://lidarr.audio/docs/api/) * [Whisparr >= v3](https://whisparr.com/docs/api/) diff --git a/screenshots/add_new_movie.png b/screenshots/radarr/add_new_movie.png similarity index 100% rename from screenshots/add_new_movie.png rename to screenshots/radarr/add_new_movie.png diff --git a/screenshots/collection_details.png b/screenshots/radarr/collection_details.png similarity index 100% rename from screenshots/collection_details.png rename to screenshots/radarr/collection_details.png diff --git a/screenshots/indexers.png b/screenshots/radarr/indexers.png similarity index 100% rename from screenshots/indexers.png rename to screenshots/radarr/indexers.png diff --git a/screenshots/logs.png b/screenshots/radarr/logs.png similarity index 100% rename from screenshots/logs.png rename to screenshots/radarr/logs.png diff --git a/screenshots/manual_search.png b/screenshots/radarr/manual_search.png similarity index 100% rename from screenshots/manual_search.png rename to screenshots/radarr/manual_search.png diff --git a/screenshots/new_movie_search.png b/screenshots/radarr/new_movie_search.png similarity index 100% rename from screenshots/new_movie_search.png rename to screenshots/radarr/new_movie_search.png diff --git a/screenshots/library.png b/screenshots/radarr/radarr_library.png similarity index 100% rename from screenshots/library.png rename to screenshots/radarr/radarr_library.png diff --git a/screenshots/sonarr/add_series.png b/screenshots/sonarr/add_series.png new file mode 100644 index 0000000..6a2907a Binary files /dev/null and b/screenshots/sonarr/add_series.png differ diff --git a/screenshots/sonarr/manual_episode_search.png b/screenshots/sonarr/manual_episode_search.png new file mode 100644 index 0000000..69001fe Binary files /dev/null and b/screenshots/sonarr/manual_episode_search.png differ diff --git a/screenshots/sonarr/season_details.png b/screenshots/sonarr/season_details.png new file mode 100644 index 0000000..c34c782 Binary files /dev/null and b/screenshots/sonarr/season_details.png differ diff --git a/screenshots/sonarr/series_details.png b/screenshots/sonarr/series_details.png new file mode 100644 index 0000000..6d60c70 Binary files /dev/null and b/screenshots/sonarr/series_details.png differ diff --git a/screenshots/sonarr/sonarr_library.png b/screenshots/sonarr/sonarr_library.png new file mode 100644 index 0000000..e10429d Binary files /dev/null and b/screenshots/sonarr/sonarr_library.png differ diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index 2fb02a0..a3902f2 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -19,6 +19,7 @@ mod tests { assert_eq!(app.navigation_stack, vec![DEFAULT_ROUTE]); assert!(app.network_tx.is_none()); assert!(!app.cancellation_token.is_cancelled()); + assert!(app.is_first_render); assert_eq!(app.error, HorizontallyScrollableText::default()); assert_eq!(app.server_tabs.index, 0); assert_eq!( @@ -55,14 +56,11 @@ mod tests { fn test_navigation_stack_methods() { let mut app = App::default(); - assert_eq!(app.get_current_route(), &DEFAULT_ROUTE); + assert_eq!(app.get_current_route(), DEFAULT_ROUTE); app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into()); assert!(app.is_routing); app.is_routing = false; @@ -70,20 +68,20 @@ mod tests { assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert!(app.is_routing); app.is_routing = false; app.pop_navigation_stack(); - assert_eq!(app.get_current_route(), &DEFAULT_ROUTE); + assert_eq!(app.get_current_route(), DEFAULT_ROUTE); assert!(app.is_routing); app.is_routing = false; app.pop_navigation_stack(); - assert_eq!(app.get_current_route(), &DEFAULT_ROUTE); + assert_eq!(app.get_current_route(), DEFAULT_ROUTE); assert!(app.is_routing); } @@ -120,19 +118,23 @@ mod tests { #[test] fn test_reset() { + let radarr_data = RadarrData { + version: "test".into(), + ..RadarrData::default() + }; + let sonarr_data = SonarrData { + version: "test".into(), + ..SonarrData::default() + }; + let data = Data { + radarr_data, + sonarr_data, + }; let mut app = App { tick_count: 2, error: "Test error".to_owned().into(), - data: Data { - radarr_data: RadarrData { - version: "test".to_owned(), - ..RadarrData::default() - }, - sonarr_data: SonarrData { - version: "test".to_owned(), - ..SonarrData::default() - }, - }, + is_first_render: false, + data, ..App::default() }; @@ -140,6 +142,7 @@ mod tests { assert_eq!(app.tick_count, 0); assert_eq!(app.error, HorizontallyScrollableText::default()); + assert!(app.is_first_render); assert!(app.data.radarr_data.version.is_empty()); assert!(app.data.sonarr_data.version.is_empty()); } @@ -188,12 +191,13 @@ mod tests { let mut app = App { tick_until_poll: 2, network_tx: Some(sync_network_tx), + is_first_render: true, ..App::default() }; assert_eq!(app.tick_count, 0); - app.on_tick(true).await; + app.on_tick().await; assert_eq!( sync_network_rx.recv().await.unwrap(), @@ -219,6 +223,14 @@ mod tests { sync_network_rx.recv().await.unwrap(), RadarrEvent::GetStatus.into() ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::GetMovies.into() @@ -237,7 +249,7 @@ mod tests { ..App::default() }; - app.on_tick(false).await; + app.on_tick().await; assert!(!app.is_routing); } @@ -250,7 +262,7 @@ mod tests { ..App::default() }; - app.on_tick(false).await; + app.on_tick().await; assert!(!app.should_refresh); } diff --git a/src/app/context_clues.rs b/src/app/context_clues.rs index 6725eaf..32a6463 100644 --- a/src/app/context_clues.rs +++ b/src/app/context_clues.rs @@ -14,10 +14,77 @@ pub fn build_context_clue_string(context_clues: &[(KeyBinding, &str)]) -> String .join(" | ") } -pub static SERVARR_CONTEXT_CLUES: [ContextClue; 2] = [ - (DEFAULT_KEYBINDINGS.tab, "change servarr"), +pub static SERVARR_CONTEXT_CLUES: [ContextClue; 3] = [ + ( + DEFAULT_KEYBINDINGS.next_servarr, + DEFAULT_KEYBINDINGS.next_servarr.desc, + ), + ( + DEFAULT_KEYBINDINGS.previous_servarr, + DEFAULT_KEYBINDINGS.previous_servarr.desc, + ), (DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc), ]; pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] = [(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)]; + +pub static BLOCKLIST_CONTEXT_CLUES: [ContextClue; 5] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), + (DEFAULT_KEYBINDINGS.clear, "clear blocklist"), +]; + +pub static CONFIRMATION_PROMPT_CONTEXT_CLUES: [ContextClue; 2] = [ + (DEFAULT_KEYBINDINGS.confirm, "submit"), + (DEFAULT_KEYBINDINGS.esc, "cancel"), +]; + +pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 3] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), + (DEFAULT_KEYBINDINGS.update, "update downloads"), +]; + +pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [ + (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), +]; + +pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [ + (DEFAULT_KEYBINDINGS.submit, "edit indexer"), + ( + DEFAULT_KEYBINDINGS.settings, + DEFAULT_KEYBINDINGS.settings.desc, + ), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), + (DEFAULT_KEYBINDINGS.test, "test indexer"), + (DEFAULT_KEYBINDINGS.test_all, "test all indexers"), + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), +]; + +pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [ + (DEFAULT_KEYBINDINGS.tasks, "open tasks"), + (DEFAULT_KEYBINDINGS.events, "open events"), + (DEFAULT_KEYBINDINGS.logs, "open logs"), + (DEFAULT_KEYBINDINGS.update, "open updates"), + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), +]; diff --git a/src/app/context_clues_tests.rs b/src/app/context_clues_tests.rs index 58699ec..0e9fdaa 100644 --- a/src/app/context_clues_tests.rs +++ b/src/app/context_clues_tests.rs @@ -2,7 +2,11 @@ mod test { use pretty_assertions::{assert_eq, assert_str_eq}; - use crate::app::context_clues::{BARE_POPUP_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES}; + use crate::app::context_clues::{ + BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, + DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, + SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + }; use crate::app::{context_clues::build_context_clue_string, key_binding::DEFAULT_KEYBINDINGS}; #[test] @@ -24,8 +28,13 @@ mod test { let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.tab); - assert_str_eq!(*description, "change servarr"); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.next_servarr); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.next_servarr.desc); + + let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.previous_servarr); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.previous_servarr.desc); let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); @@ -44,4 +53,160 @@ mod test { assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_eq!(bare_popup_context_clues_iter.next(), None); } + + #[test] + fn test_downloads_context_clues() { + let mut downloads_context_clues_iter = DOWNLOADS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = downloads_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = downloads_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); + + let (key_binding, description) = downloads_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update); + assert_str_eq!(*description, "update downloads"); + assert_eq!(downloads_context_clues_iter.next(), None); + } + + #[test] + fn test_blocklist_context_clues() { + let mut blocklist_context_clues_iter = BLOCKLIST_CONTEXT_CLUES.iter(); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.clear); + assert_str_eq!(*description, "clear blocklist"); + assert_eq!(blocklist_context_clues_iter.next(), None); + } + + #[test] + fn test_confirmation_prompt_context_clues() { + let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter(); + + let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm); + assert_str_eq!(*description, "submit"); + + let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, "cancel"); + assert_eq!(confirmation_prompt_context_clues_iter.next(), None); + } + + #[test] + fn test_root_folders_context_clues() { + let mut root_folders_context_clues_iter = ROOT_FOLDERS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = root_folders_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc); + + let (key_binding, description) = root_folders_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); + + let (key_binding, description) = root_folders_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + assert_eq!(root_folders_context_clues_iter.next(), None); + } + + #[test] + fn test_indexers_context_clues() { + let mut indexers_context_clues_iter = INDEXERS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "edit indexer"); + + let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.settings); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.settings.desc); + + let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); + + let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test); + assert_str_eq!(*description, "test indexer"); + + let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test_all); + assert_str_eq!(*description, "test all indexers"); + + let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + assert_eq!(indexers_context_clues_iter.next(), None); + } + + #[test] + fn test_system_context_clues() { + let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter(); + + let (key_binding, description) = system_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.tasks); + assert_str_eq!(*description, "open tasks"); + + let (key_binding, description) = system_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.events); + assert_str_eq!(*description, "open events"); + + let (key_binding, description) = system_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.logs); + assert_str_eq!(*description, "open logs"); + + let (key_binding, description) = system_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update); + assert_str_eq!(*description, "open updates"); + + let (key_binding, description) = system_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + assert_eq!(system_context_clues_iter.next(), None); + } } diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index 44f0518..f939a6d 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -15,8 +15,11 @@ generate_keybindings! { left, right, backspace, + next_servarr, + previous_servarr, clear, search, + auto_search, settings, filter, sort, @@ -25,12 +28,12 @@ generate_keybindings! { tasks, test, test_all, + toggle_monitoring, refresh, update, events, home, end, - tab, delete, submit, confirm, @@ -69,16 +72,28 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { key: Key::Backspace, desc: "backspace", }, + next_servarr: KeyBinding { + key: Key::Tab, + desc: "next servarr", + }, + previous_servarr: KeyBinding { + key: Key::BackTab, + desc: "previous servarr", + }, clear: KeyBinding { key: Key::Char('c'), desc: "clear", }, + auto_search: KeyBinding { + key: Key::Char('S'), + desc: "auto search", + }, search: KeyBinding { key: Key::Char('s'), desc: "search", }, settings: KeyBinding { - key: Key::Char('s'), + key: Key::Char('S'), desc: "settings", }, filter: KeyBinding { @@ -113,6 +128,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { key: Key::Char('T'), desc: "test all", }, + toggle_monitoring: KeyBinding { + key: Key::Char('m'), + desc: "toggle monitoring", + }, refresh: KeyBinding { key: Key::Ctrl('r'), desc: "refresh", @@ -129,10 +148,6 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { key: Key::End, desc: "end", }, - tab: KeyBinding { - key: Key::Tab, - desc: "tab", - }, delete: KeyBinding { key: Key::Delete, desc: "delete", diff --git a/src/app/key_binding_tests.rs b/src/app/key_binding_tests.rs index 1a17a95..c78c210 100644 --- a/src/app/key_binding_tests.rs +++ b/src/app/key_binding_tests.rs @@ -13,9 +13,12 @@ mod test { #[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")] #[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")] #[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, "backspace")] + #[case(DEFAULT_KEYBINDINGS.next_servarr, Key::Tab, "next servarr")] + #[case(DEFAULT_KEYBINDINGS.previous_servarr, Key::BackTab, "previous servarr")] #[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), "clear")] + #[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), "auto search")] #[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), "search")] - #[case(DEFAULT_KEYBINDINGS.settings, Key::Char('s'), "settings")] + #[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), "settings")] #[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")] #[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), "sort")] #[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), "edit")] @@ -24,11 +27,11 @@ mod test { #[case(DEFAULT_KEYBINDINGS.tasks, Key::Char('t'), "tasks")] #[case(DEFAULT_KEYBINDINGS.test, Key::Char('t'), "test")] #[case(DEFAULT_KEYBINDINGS.test_all, Key::Char('T'), "test all")] + #[case(DEFAULT_KEYBINDINGS.toggle_monitoring, Key::Char('m'), "toggle monitoring")] #[case(DEFAULT_KEYBINDINGS.refresh, Key::Ctrl('r'), "refresh")] #[case(DEFAULT_KEYBINDINGS.update, Key::Char('u'), "update")] #[case(DEFAULT_KEYBINDINGS.home, Key::Home, "home")] #[case(DEFAULT_KEYBINDINGS.end, Key::End, "end")] - #[case(DEFAULT_KEYBINDINGS.tab, Key::Tab, "tab")] #[case(DEFAULT_KEYBINDINGS.delete, Key::Delete, "delete")] #[case(DEFAULT_KEYBINDINGS.submit, Key::Enter, "submit")] #[case(DEFAULT_KEYBINDINGS.confirm, Key::Ctrl('s'), "submit")] diff --git a/src/app/mod.rs b/src/app/mod.rs index f401e1d..3579ad8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,6 +1,6 @@ use std::process; -use anyhow::anyhow; +use anyhow::{anyhow, Error}; use colored::Colorize; use log::{debug, error}; use serde::{Deserialize, Serialize}; @@ -21,6 +21,7 @@ pub mod context_clues; pub mod key_binding; mod key_binding_tests; pub mod radarr; +pub mod sonarr; const DEFAULT_ROUTE: Route = Route::Radarr(ActiveRadarrBlock::Movies, None); @@ -28,6 +29,7 @@ pub struct App<'a> { navigation_stack: Vec, network_tx: Option>, cancellation_token: CancellationToken, + pub is_first_render: bool, pub server_tabs: TabState, pub error: HorizontallyScrollableText, pub tick_until_poll: u64, @@ -76,26 +78,26 @@ impl<'a> App<'a> { self.tick_count = 0; } - // Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then #[allow(dead_code)] pub fn reset(&mut self) { self.reset_tick_count(); self.error = HorizontallyScrollableText::default(); + self.is_first_render = true; self.data = Data::default(); } - pub fn handle_error(&mut self, error: anyhow::Error) { + pub fn handle_error(&mut self, error: Error) { if self.error.text.is_empty() { self.error = error.to_string().into(); } } - pub async fn on_tick(&mut self, is_first_render: bool) { + pub async fn on_tick(&mut self) { if self.tick_count % self.tick_until_poll == 0 || self.is_routing || self.should_refresh { - if let Route::Radarr(active_radarr_block, _) = self.get_current_route() { - self - .radarr_on_tick(*active_radarr_block, is_first_render) - .await; + match self.get_current_route() { + Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await, + Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await, + _ => (), } self.is_routing = false; @@ -130,8 +132,8 @@ impl<'a> App<'a> { self.push_navigation_stack(route); } - pub fn get_current_route(&self) -> &Route { - self.navigation_stack.last().unwrap_or(&DEFAULT_ROUTE) + pub fn get_current_route(&self) -> Route { + *self.navigation_stack.last().unwrap_or(&DEFAULT_ROUTE) } } @@ -142,6 +144,7 @@ impl<'a> Default for App<'a> { network_tx: None, cancellation_token: CancellationToken::new(), error: HorizontallyScrollableText::default(), + is_first_render: true, server_tabs: TabState::new(vec![ TabRoute { title: "Radarr", @@ -176,7 +179,7 @@ impl<'a> Default for App<'a> { #[derive(Default)] pub struct Data<'a> { pub radarr_data: RadarrData<'a>, - pub sonarr_data: SonarrData, + pub sonarr_data: SonarrData<'a>, } #[derive(Debug, Deserialize, Serialize, Default, Clone)] diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index 1c7f181..173b2c4 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -17,11 +17,23 @@ impl<'a> App<'a> { .await; } ActiveRadarrBlock::Collections => { + self + .dispatch_network_event(RadarrEvent::GetQualityProfiles.into()) + .await; self .dispatch_network_event(RadarrEvent::GetCollections.into()) .await; + self + .dispatch_network_event(RadarrEvent::GetMovies.into()) + .await; } ActiveRadarrBlock::CollectionDetails => { + self + .dispatch_network_event(RadarrEvent::GetQualityProfiles.into()) + .await; + self + .dispatch_network_event(RadarrEvent::GetTags.into()) + .await; self.is_loading = true; self.populate_movie_collection_table().await; self.is_loading = false; @@ -37,6 +49,12 @@ impl<'a> App<'a> { .await; } ActiveRadarrBlock::Movies => { + self + .dispatch_network_event(RadarrEvent::GetQualityProfiles.into()) + .await; + self + .dispatch_network_event(RadarrEvent::GetTags.into()) + .await; self .dispatch_network_event(RadarrEvent::GetMovies.into()) .await; @@ -45,6 +63,9 @@ impl<'a> App<'a> { .await; } ActiveRadarrBlock::Indexers => { + self + .dispatch_network_event(RadarrEvent::GetTags.into()) + .await; self .dispatch_network_event(RadarrEvent::GetIndexers.into()) .await; @@ -119,11 +140,11 @@ impl<'a> App<'a> { _ => (), } - self.check_for_prompt_action().await; + self.check_for_radarr_prompt_action().await; self.reset_tick_count(); } - async fn check_for_prompt_action(&mut self) { + async fn check_for_radarr_prompt_action(&mut self) { if self.data.radarr_data.prompt_confirm { self.data.radarr_data.prompt_confirm = false; if let Some(radarr_event) = &self.data.radarr_data.prompt_confirm_action { @@ -136,19 +157,17 @@ impl<'a> App<'a> { } } - pub(super) async fn radarr_on_tick( - &mut self, - active_radarr_block: ActiveRadarrBlock, - is_first_render: bool, - ) { - if is_first_render { - self.refresh_metadata().await; + pub(super) async fn radarr_on_tick(&mut self, active_radarr_block: ActiveRadarrBlock) { + if self.is_first_render { + self.refresh_radarr_metadata().await; self.dispatch_by_radarr_block(&active_radarr_block).await; + self.is_first_render = false; + return; } if self.should_refresh { self.dispatch_by_radarr_block(&active_radarr_block).await; - self.refresh_metadata().await; + self.refresh_radarr_metadata().await; } if self.is_routing { @@ -156,16 +175,15 @@ impl<'a> App<'a> { self.cancellation_token.cancel(); } else { self.dispatch_by_radarr_block(&active_radarr_block).await; - self.refresh_metadata().await; } } if self.tick_count % self.tick_until_poll == 0 { - self.refresh_metadata().await; + self.refresh_radarr_metadata().await; } } - async fn refresh_metadata(&mut self) { + async fn refresh_radarr_metadata(&mut self) { self .dispatch_network_event(RadarrEvent::GetQualityProfiles.into()) .await; diff --git a/src/app/radarr/radarr_context_clues.rs b/src/app/radarr/radarr_context_clues.rs index a9d6a40..d35222b 100644 --- a/src/app/radarr/radarr_context_clues.rs +++ b/src/app/radarr/radarr_context_clues.rs @@ -35,60 +35,6 @@ pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [ (DEFAULT_KEYBINDINGS.esc, "cancel filter"), ]; -pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 2] = [ - ( - DEFAULT_KEYBINDINGS.refresh, - DEFAULT_KEYBINDINGS.refresh.desc, - ), - (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), -]; - -pub static BLOCKLIST_CONTEXT_CLUES: [ContextClue; 5] = [ - ( - DEFAULT_KEYBINDINGS.refresh, - DEFAULT_KEYBINDINGS.refresh.desc, - ), - (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), - (DEFAULT_KEYBINDINGS.submit, "details"), - (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), - (DEFAULT_KEYBINDINGS.clear, "clear blocklist"), -]; - -pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [ - (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), - (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), - ( - DEFAULT_KEYBINDINGS.refresh, - DEFAULT_KEYBINDINGS.refresh.desc, - ), -]; - -pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [ - (DEFAULT_KEYBINDINGS.submit, "edit indexer"), - ( - DEFAULT_KEYBINDINGS.settings, - DEFAULT_KEYBINDINGS.settings.desc, - ), - (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), - (DEFAULT_KEYBINDINGS.test, "test indexer"), - (DEFAULT_KEYBINDINGS.test_all, "test all indexers"), - ( - DEFAULT_KEYBINDINGS.refresh, - DEFAULT_KEYBINDINGS.refresh.desc, - ), -]; - -pub static SYSTEM_CONTEXT_CLUES: [ContextClue; 5] = [ - (DEFAULT_KEYBINDINGS.tasks, "open tasks"), - (DEFAULT_KEYBINDINGS.events, "open events"), - (DEFAULT_KEYBINDINGS.logs, "open logs"), - (DEFAULT_KEYBINDINGS.update, "open updates"), - ( - DEFAULT_KEYBINDINGS.refresh, - DEFAULT_KEYBINDINGS.refresh.desc, - ), -]; - pub static MOVIE_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [ ( DEFAULT_KEYBINDINGS.refresh, @@ -96,7 +42,10 @@ pub static MOVIE_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [ ), (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), - (DEFAULT_KEYBINDINGS.search, "auto search"), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; @@ -108,7 +57,10 @@ pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [ (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), - (DEFAULT_KEYBINDINGS.search, "auto search"), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; @@ -120,17 +72,13 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [ (DEFAULT_KEYBINDINGS.esc, "edit search"), ]; -pub static CONFIRMATION_PROMPT_CONTEXT_CLUES: [ContextClue; 2] = [ - (DEFAULT_KEYBINDINGS.confirm, "submit"), - (DEFAULT_KEYBINDINGS.esc, "cancel"), -]; - pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ (DEFAULT_KEYBINDINGS.submit, "start task"), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; -pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 2] = [ +pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [ (DEFAULT_KEYBINDINGS.submit, "show overview/add movie"), + (DEFAULT_KEYBINDINGS.edit, "edit collection"), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; diff --git a/src/app/radarr/radarr_context_clues_tests.rs b/src/app/radarr/radarr_context_clues_tests.rs index b20475a..91c2528 100644 --- a/src/app/radarr/radarr_context_clues_tests.rs +++ b/src/app/radarr/radarr_context_clues_tests.rs @@ -4,11 +4,10 @@ mod tests { use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::radarr_context_clues::{ - ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, - COLLECTION_DETAILS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, - INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, - MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, - SYSTEM_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, + ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, + COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, + MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, + MOVIE_DETAILS_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, }; #[test] @@ -113,141 +112,6 @@ mod tests { assert_eq!(collections_context_clues.next(), None); } - #[test] - fn test_downloads_context_clues() { - let mut downloads_context_clues_iter = DOWNLOADS_CONTEXT_CLUES.iter(); - - let (key_binding, description) = downloads_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); - - let (key_binding, description) = downloads_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); - assert_eq!(downloads_context_clues_iter.next(), None); - } - - #[test] - fn test_blocklist_context_clues() { - let mut blocklist_context_clues_iter = BLOCKLIST_CONTEXT_CLUES.iter(); - - let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); - - let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); - - let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); - assert_str_eq!(*description, "details"); - - let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); - - let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.clear); - assert_str_eq!(*description, "clear blocklist"); - assert_eq!(blocklist_context_clues_iter.next(), None); - } - - #[test] - fn test_root_folders_context_clues() { - let mut root_folders_context_clues_iter = ROOT_FOLDERS_CONTEXT_CLUES.iter(); - - let (key_binding, description) = root_folders_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc); - - let (key_binding, description) = root_folders_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); - - let (key_binding, description) = root_folders_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); - assert_eq!(root_folders_context_clues_iter.next(), None); - } - - #[test] - fn test_indexers_context_clues() { - let mut indexers_context_clues_iter = INDEXERS_CONTEXT_CLUES.iter(); - - let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); - assert_str_eq!(*description, "edit indexer"); - - let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.settings); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.settings.desc); - - let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); - - let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test); - assert_str_eq!(*description, "test indexer"); - - let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.test_all); - assert_str_eq!(*description, "test all indexers"); - - let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); - assert_eq!(indexers_context_clues_iter.next(), None); - } - - #[test] - fn test_system_context_clues() { - let mut system_context_clues_iter = SYSTEM_CONTEXT_CLUES.iter(); - - let (key_binding, description) = system_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.tasks); - assert_str_eq!(*description, "open tasks"); - - let (key_binding, description) = system_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.events); - assert_str_eq!(*description, "open events"); - - let (key_binding, description) = system_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.logs); - assert_str_eq!(*description, "open logs"); - - let (key_binding, description) = system_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update); - assert_str_eq!(*description, "open updates"); - - let (key_binding, description) = system_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); - assert_eq!(system_context_clues_iter.next(), None); - } - #[test] fn test_movie_details_context_clues() { let mut movie_details_context_clues_iter = MOVIE_DETAILS_CONTEXT_CLUES.iter(); @@ -269,8 +133,8 @@ mod tests { let (key_binding, description) = movie_details_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); - assert_str_eq!(*description, "auto search"); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); let (key_binding, description) = movie_details_context_clues_iter.next().unwrap(); @@ -305,8 +169,8 @@ mod tests { let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); - assert_str_eq!(*description, "auto search"); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap(); @@ -349,22 +213,6 @@ mod tests { assert_eq!(add_movie_search_results_context_clues_iter.next(), None); } - #[test] - fn test_confirmation_prompt_context_clues() { - let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter(); - - let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm); - assert_str_eq!(*description, "submit"); - - let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); - assert_str_eq!(*description, "cancel"); - assert_eq!(confirmation_prompt_context_clues_iter.next(), None); - } - #[test] fn test_system_tasks_context_clues() { let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter(); @@ -392,6 +240,11 @@ mod tests { let (key_binding, description) = collection_details_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit); + assert_str_eq!(*description, "edit collection"); + + let (key_binding, description) = collection_details_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_eq!(collection_details_context_clues_iter.next(), None); diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 901ca12..45648d7 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -38,17 +38,25 @@ mod tests { .await; assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::GetCollections.into() ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetMovies.into() + ); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); } #[tokio::test] async fn test_dispatch_by_collection_details_block() { - let (mut app, _) = construct_app_unit(); + let (mut app, mut sync_network_rx) = construct_app_unit(); app.data.radarr_data.collections.set_items(vec![Collection { movies: Some(vec![CollectionMovie::default()]), @@ -60,6 +68,14 @@ mod tests { .await; assert!(!app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); assert!(!app.data.radarr_data.collection_movies.items.is_empty()); assert_eq!(app.tick_count, 0); assert!(!app.data.radarr_data.prompt_confirm); @@ -80,6 +96,14 @@ mod tests { .await; assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::AddMovie(None).into() @@ -132,6 +156,14 @@ mod tests { .await; assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::GetMovies.into() @@ -153,6 +185,10 @@ mod tests { .await; assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetTags.into() + ); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::GetIndexers.into() @@ -459,22 +495,22 @@ mod tests { } #[tokio::test] - async fn test_check_for_prompt_action_no_prompt_confirm() { + async fn test_check_for_radarr_prompt_action_no_prompt_confirm() { let mut app = App::default(); app.data.radarr_data.prompt_confirm = false; - app.check_for_prompt_action().await; + app.check_for_radarr_prompt_action().await; assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.should_refresh); } #[tokio::test] - async fn test_check_for_prompt_action() { + async fn test_check_for_radarr_prompt_action() { let (mut app, mut sync_network_rx) = construct_app_unit(); app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::GetStatus); - app.check_for_prompt_action().await; + app.check_for_radarr_prompt_action().await; assert!(!app.data.radarr_data.prompt_confirm); assert_eq!( @@ -490,7 +526,7 @@ mod tests { let (mut app, mut sync_network_rx) = construct_app_unit(); app.is_routing = true; - app.refresh_metadata().await; + app.refresh_radarr_metadata().await; assert_eq!( sync_network_rx.recv().await.unwrap(), @@ -522,8 +558,9 @@ mod tests { #[tokio::test] async fn test_radarr_on_tick_first_render() { let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_first_render = true; - app.radarr_on_tick(ActiveRadarrBlock::Downloads, true).await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; assert_eq!( sync_network_rx.recv().await.unwrap(), @@ -551,6 +588,7 @@ mod tests { ); assert!(app.is_loading); assert!(!app.data.radarr_data.prompt_confirm); + assert!(!app.is_first_render); } #[tokio::test] @@ -559,26 +597,8 @@ mod tests { app.is_routing = true; app.should_refresh = true; - app - .radarr_on_tick(ActiveRadarrBlock::Downloads, false) - .await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetDownloads.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetQualityProfiles.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetTags.into() - ); - assert_eq!( - sync_network_rx.recv().await.unwrap(), - RadarrEvent::GetRootFolders.into() - ); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::GetDownloads.into() @@ -592,9 +612,7 @@ mod tests { app.is_routing = true; app.should_refresh = false; - app - .radarr_on_tick(ActiveRadarrBlock::Downloads, false) - .await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; assert!(app.cancellation_token.is_cancelled()); } @@ -604,9 +622,7 @@ mod tests { let (mut app, mut sync_network_rx) = construct_app_unit(); app.should_refresh = true; - app - .radarr_on_tick(ActiveRadarrBlock::Downloads, false) - .await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; assert_eq!( sync_network_rx.recv().await.unwrap(), @@ -623,9 +639,7 @@ mod tests { app.is_routing = true; app.should_refresh = true; - app - .radarr_on_tick(ActiveRadarrBlock::Downloads, false) - .await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; assert_eq!( sync_network_rx.recv().await.unwrap(), @@ -643,9 +657,7 @@ mod tests { app.tick_count = 2; app.tick_until_poll = 2; - app - .radarr_on_tick(ActiveRadarrBlock::Downloads, false) - .await; + app.radarr_on_tick(ActiveRadarrBlock::Downloads).await; assert_eq!( sync_network_rx.recv().await.unwrap(), @@ -701,6 +713,7 @@ mod tests { let mut app = App { network_tx: Some(sync_network_tx), tick_count: 1, + is_first_render: false, ..App::default() }; app.data.radarr_data.prompt_confirm = true; diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs new file mode 100644 index 0000000..3d2bb82 --- /dev/null +++ b/src/app/sonarr/mod.rs @@ -0,0 +1,245 @@ +use crate::{ + models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock, + network::sonarr_network::SonarrEvent, +}; + +use super::App; + +pub mod sonarr_context_clues; + +#[cfg(test)] +#[path = "sonarr_tests.rs"] +mod sonarr_tests; + +impl<'a> App<'a> { + pub(super) async fn dispatch_by_sonarr_block(&mut self, active_sonarr_block: &ActiveSonarrBlock) { + match active_sonarr_block { + ActiveSonarrBlock::Series => { + self + .dispatch_network_event(SonarrEvent::GetQualityProfiles.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetLanguageProfiles.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetTags.into()) + .await; + self + .dispatch_network_event(SonarrEvent::ListSeries.into()) + .await; + } + ActiveSonarrBlock::SeriesDetails => { + self + .dispatch_network_event(SonarrEvent::ListSeries.into()) + .await; + self.is_loading = true; + self.populate_seasons_table().await; + self.is_loading = false; + } + ActiveSonarrBlock::SeriesHistory => { + self + .dispatch_network_event(SonarrEvent::GetSeriesHistory(None).into()) + .await; + } + ActiveSonarrBlock::SeasonDetails => { + self + .dispatch_network_event(SonarrEvent::GetEpisodes(None).into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetEpisodeFiles(None).into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetDownloads.into()) + .await; + } + ActiveSonarrBlock::SeasonHistory => { + self + .dispatch_network_event(SonarrEvent::GetSeasonHistory(None).into()) + .await; + } + ActiveSonarrBlock::ManualSeasonSearch => { + match self.data.sonarr_data.season_details_modal.as_ref() { + Some(season_details_modal) if season_details_modal.season_releases.is_empty() => { + self + .dispatch_network_event(SonarrEvent::GetSeasonReleases(None).into()) + .await; + } + _ => (), + } + } + ActiveSonarrBlock::EpisodeDetails | ActiveSonarrBlock::EpisodeFile => { + self + .dispatch_network_event(SonarrEvent::GetEpisodeDetails(None).into()) + .await; + } + ActiveSonarrBlock::EpisodeHistory => { + self + .dispatch_network_event(SonarrEvent::GetEpisodeHistory(None).into()) + .await; + } + ActiveSonarrBlock::ManualEpisodeSearch => { + if let Some(season_details_modal) = self.data.sonarr_data.season_details_modal.as_ref() { + if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() { + if episode_details_modal.episode_releases.is_empty() { + self + .dispatch_network_event(SonarrEvent::GetEpisodeReleases(None).into()) + .await; + } + } + } + } + ActiveSonarrBlock::Downloads => { + self + .dispatch_network_event(SonarrEvent::GetDownloads.into()) + .await; + } + ActiveSonarrBlock::Blocklist => { + self + .dispatch_network_event(SonarrEvent::ListSeries.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetBlocklist.into()) + .await; + } + ActiveSonarrBlock::History => { + self + .dispatch_network_event(SonarrEvent::GetHistory(None).into()) + .await; + } + ActiveSonarrBlock::RootFolders => { + self + .dispatch_network_event(SonarrEvent::GetRootFolders.into()) + .await; + } + ActiveSonarrBlock::Indexers => { + self + .dispatch_network_event(SonarrEvent::GetTags.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetIndexers.into()) + .await; + } + ActiveSonarrBlock::AllIndexerSettingsPrompt => { + self + .dispatch_network_event(SonarrEvent::GetAllIndexerSettings.into()) + .await; + } + ActiveSonarrBlock::TestIndexer => { + self + .dispatch_network_event(SonarrEvent::TestIndexer(None).into()) + .await; + } + ActiveSonarrBlock::TestAllIndexers => { + self + .dispatch_network_event(SonarrEvent::TestAllIndexers.into()) + .await; + } + ActiveSonarrBlock::System => { + self + .dispatch_network_event(SonarrEvent::GetTasks.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetQueuedEvents.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetLogs(None).into()) + .await; + } + ActiveSonarrBlock::AddSeriesSearchResults => { + self + .dispatch_network_event(SonarrEvent::SearchNewSeries(None).into()) + .await; + } + ActiveSonarrBlock::SystemUpdates => { + self + .dispatch_network_event(SonarrEvent::GetUpdates.into()) + .await; + } + _ => (), + } + + self.check_for_sonarr_prompt_action().await; + self.reset_tick_count(); + } + + async fn check_for_sonarr_prompt_action(&mut self) { + if self.data.sonarr_data.prompt_confirm { + self.data.sonarr_data.prompt_confirm = false; + if let Some(sonarr_event) = &self.data.sonarr_data.prompt_confirm_action { + self + .dispatch_network_event(sonarr_event.clone().into()) + .await; + self.should_refresh = true; + self.data.sonarr_data.prompt_confirm_action = None; + } + } + } + + pub(super) async fn sonarr_on_tick(&mut self, active_sonarr_block: ActiveSonarrBlock) { + if self.is_first_render { + self.refresh_sonarr_metadata().await; + self.dispatch_by_sonarr_block(&active_sonarr_block).await; + self.is_first_render = false; + return; + } + + if self.should_refresh { + self.dispatch_by_sonarr_block(&active_sonarr_block).await; + self.refresh_sonarr_metadata().await; + } + + if self.is_routing { + if !self.should_refresh { + self.cancellation_token.cancel(); + } else { + self.dispatch_by_sonarr_block(&active_sonarr_block).await; + } + } + + if self.tick_count % self.tick_until_poll == 0 { + self.refresh_sonarr_metadata().await; + } + } + + async fn refresh_sonarr_metadata(&mut self) { + self + .dispatch_network_event(SonarrEvent::GetQualityProfiles.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetLanguageProfiles.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetTags.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetRootFolders.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetDownloads.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetDiskSpace.into()) + .await; + self + .dispatch_network_event(SonarrEvent::GetStatus.into()) + .await; + } + + async fn populate_seasons_table(&mut self) { + let seasons = self + .data + .sonarr_data + .series + .current_selection() + .clone() + .seasons + .unwrap_or_default() + .into_iter() + .map(|mut season| { + season.title = Some(format!("Season {}", season.season_number)); + season + }) + .collect(); + self.data.sonarr_data.seasons.set_items(seasons); + } +} diff --git a/src/app/sonarr/sonarr_context_clues.rs b/src/app/sonarr/sonarr_context_clues.rs new file mode 100644 index 0000000..cfb1945 --- /dev/null +++ b/src/app/sonarr/sonarr_context_clues.rs @@ -0,0 +1,159 @@ +use crate::app::{context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS}; + +#[cfg(test)] +#[path = "sonarr_context_clues_tests.rs"] +mod sonarr_context_clues_tests; + +pub static ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [ + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, "edit search"), +]; + +pub static SERIES_CONTEXT_CLUES: [ContextClue; 10] = [ + (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), + (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.update, "update all"), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, "cancel filter"), +]; + +pub static SERIES_DETAILS_CONTEXT_CLUES: [ContextClue; 8] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), + ( + DEFAULT_KEYBINDINGS.toggle_monitoring, + DEFAULT_KEYBINDINGS.toggle_monitoring.desc, + ), + (DEFAULT_KEYBINDINGS.submit, "season details"), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.esc, "cancel filter"), +]; + +pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), + (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), +]; + +pub static SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 2] = [ + (DEFAULT_KEYBINDINGS.submit, "episode details"), + (DEFAULT_KEYBINDINGS.delete, "delete episode"), +]; + +pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + ( + DEFAULT_KEYBINDINGS.toggle_monitoring, + DEFAULT_KEYBINDINGS.toggle_monitoring.desc, + ), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), +]; + +pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 1] = + [(DEFAULT_KEYBINDINGS.submit, "details")]; + +pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + +pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ + (DEFAULT_KEYBINDINGS.submit, "start task"), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; diff --git a/src/app/sonarr/sonarr_context_clues_tests.rs b/src/app/sonarr/sonarr_context_clues_tests.rs new file mode 100644 index 0000000..46ec883 --- /dev/null +++ b/src/app/sonarr/sonarr_context_clues_tests.rs @@ -0,0 +1,402 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::{assert_eq, assert_str_eq}; + + use crate::app::{ + key_binding::DEFAULT_KEYBINDINGS, + sonarr::sonarr_context_clues::{ + ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, DETAILS_CONTEXTUAL_CONTEXT_CLUES, + EPISODE_DETAILS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, + MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, + SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, + SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, + }, + }; + + #[test] + fn test_add_series_search_results_context_clues() { + let mut add_series_search_results_context_clues_iter = + ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = add_series_search_results_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = add_series_search_results_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, "edit search"); + assert_eq!(add_series_search_results_context_clues_iter.next(), None); + } + + #[test] + fn test_series_context_clues() { + let mut series_context_clues_iter = SERIES_CONTEXT_CLUES.iter(); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update); + assert_str_eq!(*description, "update all"); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = series_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, "cancel filter"); + assert_eq!(series_context_clues_iter.next(), None); + } + + #[test] + fn test_series_history_context_clues() { + let mut series_history_context_clues_iter = SERIES_HISTORY_CONTEXT_CLUES.iter(); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.update.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, "cancel filter/close"); + assert_eq!(series_history_context_clues_iter.next(), None); + } + + #[test] + fn test_history_context_clues() { + let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter(); + + let (key_binding, description) = history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc); + + let (key_binding, description) = history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc); + + let (key_binding, description) = history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, "cancel filter"); + assert_eq!(history_context_clues_iter.next(), None); + } + + #[test] + fn test_series_details_context_clues() { + let mut series_details_context_clues_iter = SERIES_DETAILS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = series_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = series_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc); + + let (key_binding, description) = series_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.toggle_monitoring); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.toggle_monitoring.desc); + + let (key_binding, description) = series_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "season details"); + + let (key_binding, description) = series_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc); + + let (key_binding, description) = series_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.update.desc); + + let (key_binding, description) = series_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); + + let (key_binding, description) = series_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); + assert_eq!(series_details_context_clues_iter.next(), None); + } + + #[test] + fn test_season_details_context_clues() { + let mut season_details_context_clues_iter = SEASON_DETAILS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = season_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = season_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.toggle_monitoring); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.toggle_monitoring.desc); + + let (key_binding, description) = season_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc); + + let (key_binding, description) = season_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); + + let (key_binding, description) = season_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); + assert_eq!(season_details_context_clues_iter.next(), None); + } + + #[test] + fn test_season_details_contextual_context_clues() { + let mut season_details_contextual_context_clues_iter = + SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES.iter(); + let (key_binding, description) = season_details_contextual_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "episode details"); + + let (key_binding, description) = season_details_contextual_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); + assert_str_eq!(*description, "delete episode"); + assert_eq!(season_details_contextual_context_clues_iter.next(), None); + } + + #[test] + fn test_season_history_context_clues() { + let mut season_history_context_clues_iter = SEASON_HISTORY_CONTEXT_CLUES.iter(); + let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc); + + let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc); + + let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); + + let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, "cancel filter/close"); + assert_eq!(season_history_context_clues_iter.next(), None); + } + + #[test] + fn test_manual_season_search_context_clues() { + let mut manual_season_search_context_clues_iter = MANUAL_SEASON_SEARCH_CONTEXT_CLUES.iter(); + + let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); + + let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); + assert_eq!(manual_season_search_context_clues_iter.next(), None); + } + + #[test] + fn test_manual_episode_search_context_clues() { + let mut manual_episode_search_context_clues_iter = MANUAL_EPISODE_SEARCH_CONTEXT_CLUES.iter(); + + let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); + + let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); + assert_eq!(manual_episode_search_context_clues_iter.next(), None); + } + + #[test] + fn details_contextual_context_clues() { + let mut manual_search_contextual_context_clues_iter = DETAILS_CONTEXTUAL_CONTEXT_CLUES.iter(); + let (key_binding, description) = manual_search_contextual_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + assert_eq!(manual_search_contextual_context_clues_iter.next(), None); + } + + #[test] + fn test_episode_details_context_clues() { + let mut episode_details_context_clues_iter = EPISODE_DETAILS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = episode_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = episode_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); + + let (key_binding, description) = episode_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); + assert_eq!(episode_details_context_clues_iter.next(), None); + } + + #[test] + fn test_system_tasks_context_clues() { + let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = system_tasks_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "start task"); + + let (key_binding, description) = system_tasks_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); + assert_eq!(system_tasks_context_clues_iter.next(), None); + } +} diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs new file mode 100644 index 0000000..8237f7a --- /dev/null +++ b/src/app/sonarr/sonarr_tests.rs @@ -0,0 +1,743 @@ +#[cfg(test)] +mod tests { + mod sonarr_tests { + use pretty_assertions::{assert_eq, assert_str_eq}; + use tokio::sync::mpsc; + + use crate::{ + app::App, + models::{ + servarr_data::sonarr::{ + modals::{EpisodeDetailsModal, SeasonDetailsModal}, + sonarr_data::ActiveSonarrBlock, + }, + sonarr_models::{Season, Series, SonarrRelease}, + }, + network::{sonarr_network::SonarrEvent, NetworkEvent}, + }; + + #[tokio::test] + async fn test_dispatch_by_blocklist_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::Blocklist) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::ListSeries.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetBlocklist.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_series_history_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::SeriesHistory) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetSeriesHistory(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_series_details_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app.data.sonarr_data.series.set_items(vec![Series { + seasons: Some(vec![Season::default()]), + ..Series::default() + }]); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::SeriesDetails) + .await; + + assert!(!app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::ListSeries.into() + ); + assert!(!app.data.sonarr_data.seasons.items.is_empty()); + assert_eq!(app.tick_count, 0); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[tokio::test] + async fn test_dispatch_by_season_details_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonDetails) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetEpisodes(None).into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetEpisodeFiles(None).into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDownloads.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_season_history_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::SeasonHistory) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetSeasonHistory(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_season_search_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetSeasonReleases(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_season_search_block_is_loading() { + let mut app = App { + is_loading: true, + ..App::default() + }; + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch) + .await; + + assert!(app.is_loading); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_season_search_block_season_releases_non_empty() { + let mut app = App::default(); + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal + .season_releases + .set_items(vec![SonarrRelease::default()]); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch) + .await; + + assert!(!app.is_loading); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_episode_details_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeDetails) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetEpisodeDetails(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_episode_file_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeFile) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetEpisodeDetails(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_episode_history_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::EpisodeHistory) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetEpisodeHistory(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_episode_search_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + let season_details_modal = SeasonDetailsModal { + episode_details_modal: Some(EpisodeDetailsModal::default()), + ..SeasonDetailsModal::default() + }; + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualEpisodeSearch) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetEpisodeReleases(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_episode_search_block_is_loading() { + let mut app = App { + is_loading: true, + ..App::default() + }; + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualEpisodeSearch) + .await; + + assert!(app.is_loading); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_manual_episode_search_block_episode_releases_non_empty() { + let mut app = App::default(); + let mut episode_details_modal = EpisodeDetailsModal::default(); + episode_details_modal + .episode_releases + .set_items(vec![SonarrRelease::default()]); + let season_details_modal = SeasonDetailsModal { + episode_details_modal: Some(episode_details_modal), + ..SeasonDetailsModal::default() + }; + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualEpisodeSearch) + .await; + + assert!(!app.is_loading); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_history_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::History) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetHistory(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_downloads_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::Downloads) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDownloads.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_root_folders_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::RootFolders) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetRootFolders.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_series_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::Series) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetLanguageProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::ListSeries.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_indexers_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::Indexers) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetIndexers.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_all_indexer_settings_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::AllIndexerSettingsPrompt) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetAllIndexerSettings.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_test_indexer_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::TestIndexer) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::TestIndexer(None).into() + ); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_test_all_indexers_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::TestAllIndexers) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::TestAllIndexers.into() + ); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_system_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::System) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetTasks.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetQueuedEvents.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetLogs(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_system_updates_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::SystemUpdates) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetUpdates.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_dispatch_by_add_series_search_results_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_sonarr_block(&ActiveSonarrBlock::AddSeriesSearchResults) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::SearchNewSeries(None).into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + + #[tokio::test] + async fn test_check_for_sonarr_prompt_action_no_prompt_confirm() { + let mut app = App::default(); + app.data.sonarr_data.prompt_confirm = false; + + app.check_for_sonarr_prompt_action().await; + + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.should_refresh); + } + + #[tokio::test] + async fn test_check_for_sonarr_prompt_action() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::GetStatus); + + app.check_for_sonarr_prompt_action().await; + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetStatus.into() + ); + assert!(app.should_refresh); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + } + + #[tokio::test] + async fn test_sonarr_refresh_metadata() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_routing = true; + + app.refresh_sonarr_metadata().await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetLanguageProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetRootFolders.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDownloads.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDiskSpace.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetStatus.into() + ); + assert!(app.is_loading); + } + + #[tokio::test] + async fn test_sonarr_on_tick_first_render() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_first_render = true; + + app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetLanguageProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetRootFolders.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDownloads.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDiskSpace.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetStatus.into() + ); + assert!(app.is_loading); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.is_first_render); + } + + #[tokio::test] + async fn test_sonarr_on_tick_routing() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_routing = true; + app.should_refresh = true; + + app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDownloads.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[tokio::test] + async fn test_sonarr_on_tick_routing_while_long_request_is_running_should_cancel_request() { + let (mut app, _) = construct_app_unit(); + app.is_routing = true; + app.should_refresh = false; + + app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await; + + assert!(app.cancellation_token.is_cancelled()); + } + + #[tokio::test] + async fn test_sonarr_on_tick_should_refresh() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.should_refresh = true; + + app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDownloads.into() + ); + assert!(app.should_refresh); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[tokio::test] + async fn test_sonarr_on_tick_should_refresh_does_not_cancel_prompt_requests() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.is_loading = true; + app.is_routing = true; + app.should_refresh = true; + + app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDownloads.into() + ); + assert!(app.is_loading); + assert!(app.should_refresh); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.cancellation_token.is_cancelled()); + } + + #[tokio::test] + async fn test_sonarr_on_tick_network_tick_frequency() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.tick_count = 2; + app.tick_until_poll = 2; + + app.sonarr_on_tick(ActiveSonarrBlock::Downloads).await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetQualityProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetLanguageProfiles.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetTags.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetRootFolders.into() + ); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + SonarrEvent::GetDownloads.into() + ); + assert!(app.is_loading); + } + + #[tokio::test] + async fn test_populate_seasons_table_unfiltered() { + let mut app = App::default(); + app.data.sonarr_data.series.set_items(vec![Series { + seasons: Some(vec![Season::default()]), + ..Series::default() + }]); + + app.populate_seasons_table().await; + + assert!(!app.data.sonarr_data.seasons.items.is_empty()); + assert_str_eq!( + app.data.sonarr_data.seasons.items[0] + .title + .as_ref() + .unwrap(), + "Season 0" + ); + } + + #[tokio::test] + async fn test_populate_seasons_table_filtered() { + let mut app = App::default(); + app.data.sonarr_data.series.set_filtered_items(vec![Series { + seasons: Some(vec![Season::default()]), + ..Series::default() + }]); + + app.populate_seasons_table().await; + + assert!(!app.data.sonarr_data.seasons.items.is_empty()); + assert_str_eq!( + app.data.sonarr_data.seasons.items[0] + .title + .as_ref() + .unwrap(), + "Season 0" + ); + } + + fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { + let (sync_network_tx, sync_network_rx) = mpsc::channel::(500); + let mut app = App { + network_tx: Some(sync_network_tx), + tick_count: 1, + is_first_render: false, + ..App::default() + }; + app.data.sonarr_data.prompt_confirm = true; + + (app, sync_network_rx) + } + } +} diff --git a/src/cli/sonarr/list_command_handler.rs b/src/cli/sonarr/list_command_handler.rs index cf92a36..1603308 100644 --- a/src/cli/sonarr/list_command_handler.rs +++ b/src/cli/sonarr/list_command_handler.rs @@ -33,6 +33,15 @@ pub enum SonarrListCommand { )] series_id: i64, }, + #[command(about = "List the episode files for the series with the given ID")] + EpisodeFiles { + #[arg( + long, + help = "The Sonarr ID of the series whose episode files you wish to fetch", + required = true + )] + series_id: i64, + }, #[command(about = "Fetch all history events for the episode with the given ID")] EpisodeHistory { #[arg( @@ -67,6 +76,23 @@ pub enum SonarrListCommand { QueuedEvents, #[command(about = "List all root folders in Sonarr")] RootFolders, + #[command( + about = "Fetch all history events for the given season corresponding to the series with the given ID." + )] + SeasonHistory { + #[arg( + long, + help = "The Sonarr ID of the series whose history you wish to fetch and list", + required = true + )] + series_id: i64, + #[arg( + long, + help = "The season number to fetch history events for", + required = true + )] + season_number: i64, + }, #[command(about = "List all series in your Sonarr library")] Series, #[command(about = "Fetch all history events for the series with the given ID")] @@ -141,6 +167,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + SonarrListCommand::EpisodeFiles { series_id } => { + let resp = self + .network + .handle_network_event(SonarrEvent::GetEpisodeFiles(Some(series_id)).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } SonarrListCommand::EpisodeHistory { episode_id } => { let resp = self .network @@ -207,6 +240,18 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrListCommand> for SonarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + SonarrListCommand::SeasonHistory { + series_id, + season_number, + } => { + let resp = self + .network + .handle_network_event( + SonarrEvent::GetSeasonHistory(Some((series_id, season_number))).into(), + ) + .await?; + serde_json::to_string_pretty(&resp)? + } SonarrListCommand::Series => { let resp = self .network diff --git a/src/cli/sonarr/list_command_handler_tests.rs b/src/cli/sonarr/list_command_handler_tests.rs index 7e71599..c333193 100644 --- a/src/cli/sonarr/list_command_handler_tests.rs +++ b/src/cli/sonarr/list_command_handler_tests.rs @@ -57,6 +57,18 @@ mod tests { ); } + #[test] + fn test_list_episode_files_requires_series_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "sonarr", "list", "episode-files"]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + #[test] fn test_list_episode_history_requires_series_id() { let result = @@ -149,6 +161,79 @@ mod tests { } } + #[test] + fn test_list_episode_files_success() { + let expected_args = SonarrListCommand::EpisodeFiles { series_id: 1 }; + let result = Cli::try_parse_from([ + "managarr", + "sonarr", + "list", + "episode-files", + "--series-id", + "1", + ]); + + assert!(result.is_ok()); + + if let Some(Command::Sonarr(SonarrCommand::List(episode_files_command))) = + result.unwrap().command + { + assert_eq!(episode_files_command, expected_args); + } + } + + #[test] + fn test_season_history_requires_series_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "list", + "season-history", + "--season-number", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_season_history_requires_season_number() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "list", + "season-history", + "--series-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_season_history_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "list", + "season-history", + "--series-id", + "1", + "--season-number", + "1", + ]); + + assert!(result.is_ok()); + } + #[test] fn test_list_series_history_requires_series_id() { let result = @@ -263,6 +348,32 @@ mod tests { assert!(result.is_ok()); } + #[tokio::test] + async fn test_handle_list_episode_files_command() { + let expected_series_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::GetEpisodeFiles(Some(expected_series_id)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let list_episode_files_command = SonarrListCommand::EpisodeFiles { series_id: 1 }; + + let result = + SonarrListCommandHandler::with(&app_arc, list_episode_files_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } + #[tokio::test] async fn test_handle_list_history_command() { let expected_events = 1000; @@ -368,5 +479,35 @@ mod tests { assert!(result.is_ok()); } + + #[tokio::test] + async fn test_list_season_history_command() { + let expected_series_id = 1; + let expected_season_number = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::GetSeasonHistory(Some((expected_series_id, expected_season_number))).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let list_season_history_command = SonarrListCommand::SeasonHistory { + series_id: 1, + season_number: 1, + }; + + let result = + SonarrListCommandHandler::with(&app_arc, list_season_history_command, &mut mock_network) + .handle() + .await; + + assert!(result.is_ok()); + } } } diff --git a/src/cli/sonarr/mod.rs b/src/cli/sonarr/mod.rs index 1cb1e42..3a19110 100644 --- a/src/cli/sonarr/mod.rs +++ b/src/cli/sonarr/mod.rs @@ -120,6 +120,32 @@ pub enum SonarrCommand { }, #[command(about = "Test all Sonarr indexers")] TestAllIndexers, + #[command(about = "Toggle monitoring for the specified episode")] + ToggleEpisodeMonitoring { + #[arg( + long, + help = "The Sonarr ID of the episode to toggle monitoring on", + required = true + )] + episode_id: i64, + }, + #[command( + about = "Toggle monitoring for the specified season that corresponds to the specified series ID" + )] + ToggleSeasonMonitoring { + #[arg( + long, + help = "The Sonarr ID of the series that the season belongs to", + required = true + )] + series_id: i64, + #[arg( + long, + help = "The season number to toggle monitoring for", + required = true + )] + season_number: i64, + }, } impl From for Command { @@ -245,6 +271,25 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrCommand> for SonarrCliHandler<'a, ' .await?; serde_json::to_string_pretty(&resp)? } + SonarrCommand::ToggleEpisodeMonitoring { episode_id } => { + let resp = self + .network + .handle_network_event(SonarrEvent::ToggleEpisodeMonitoring(Some(episode_id)).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } + SonarrCommand::ToggleSeasonMonitoring { + series_id, + season_number, + } => { + let resp = self + .network + .handle_network_event( + SonarrEvent::ToggleSeasonMonitoring(Some((series_id, season_number))).into(), + ) + .await?; + serde_json::to_string_pretty(&resp)? + } }; Ok(result) diff --git a/src/cli/sonarr/sonarr_command_tests.rs b/src/cli/sonarr/sonarr_command_tests.rs index 4490d23..7599274 100644 --- a/src/cli/sonarr/sonarr_command_tests.rs +++ b/src/cli/sonarr/sonarr_command_tests.rs @@ -142,6 +142,80 @@ mod tests { assert!(result.is_ok()); } + + #[test] + fn test_toggle_episode_monitoring_requires_episode_id() { + let result = + Cli::command().try_get_matches_from(["managarr", "sonarr", "toggle-episode-monitoring"]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_toggle_episode_monitoring_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "toggle-episode-monitoring", + "--episode-id", + "1", + ]); + + assert!(result.is_ok()); + } + + #[test] + fn test_toggle_season_monitoring_requires_series_id() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "toggle-season-monitoring", + "--season-number", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_toggle_season_monitoring_requires_season_number() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "toggle-season-monitoring", + "--series-id", + "1", + ]); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_toggle_season_monitoring_requirements_satisfied() { + let result = Cli::command().try_get_matches_from([ + "managarr", + "sonarr", + "toggle-season-monitoring", + "--series-id", + "1", + "--season-number", + "1", + ]); + + assert!(result.is_ok()); + } } mod handler { @@ -616,5 +690,69 @@ mod tests { assert!(result.is_ok()); } + + #[tokio::test] + async fn test_list_toggle_episode_monitoring_command() { + let expected_episode_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::ToggleEpisodeMonitoring(Some(expected_episode_id)).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let toggle_episode_monitoring_command = + SonarrCommand::ToggleEpisodeMonitoring { episode_id: 1 }; + + let result = SonarrCliHandler::with( + &app_arc, + toggle_episode_monitoring_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_list_toggle_season_monitoring_command() { + let expected_series_id = 1; + let expected_season_number = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + SonarrEvent::ToggleSeasonMonitoring(Some((expected_series_id, expected_season_number))) + .into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Sonarr(SonarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::default())); + let toggle_season_monitoring_command = SonarrCommand::ToggleSeasonMonitoring { + series_id: 1, + season_number: 1, + }; + + let result = SonarrCliHandler::with( + &app_arc, + toggle_season_monitoring_command, + &mut mock_network, + ) + .handle() + .await; + + assert!(result.is_ok()); + } } } diff --git a/src/event/key.rs b/src/event/key.rs index a217268..51e10fe 100644 --- a/src/event/key.rs +++ b/src/event/key.rs @@ -19,6 +19,7 @@ pub enum Key { Home, End, Tab, + BackTab, Delete, Ctrl(char), Char(char), @@ -40,6 +41,7 @@ impl Display for Key { Key::Home => write!(f, ""), Key::End => write!(f, ""), Key::Tab => write!(f, ""), + Key::BackTab => write!(f, ""), Key::Delete => write!(f, ""), _ => write!(f, "<{self:?}>"), } @@ -75,6 +77,11 @@ impl From for Key { KeyEvent { code: KeyCode::End, .. } => Key::End, + KeyEvent { + code: KeyCode::BackTab, + modifiers: KeyModifiers::SHIFT, + .. + } => Key::BackTab, KeyEvent { code: KeyCode::Tab, .. } => Key::Tab, diff --git a/src/event/key_tests.rs b/src/event/key_tests.rs index 5429aad..a06269c 100644 --- a/src/event/key_tests.rs +++ b/src/event/key_tests.rs @@ -17,6 +17,7 @@ mod tests { #[case(Key::Home, "home")] #[case(Key::End, "end")] #[case(Key::Tab, "tab")] + #[case(Key::BackTab, "shift-tab")] #[case(Key::Delete, "del")] #[case(Key::Char('q'), "q")] #[case(Key::Ctrl('q'), "ctrl-q")] @@ -67,6 +68,19 @@ mod tests { assert_eq!(Key::from(KeyEvent::from(KeyCode::Tab)), Key::Tab); } + #[test] + fn test_key_from_back_tab() { + assert_eq!( + Key::from(KeyEvent { + code: KeyCode::BackTab, + modifiers: KeyModifiers::SHIFT, + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }), + Key::BackTab + ); + } + #[test] fn test_key_from_delete() { assert_eq!(Key::from(KeyEvent::from(KeyCode::Delete)), Key::Delete); diff --git a/src/handlers/handler_test_utils.rs b/src/handlers/handler_test_utils.rs index ca71192..b3e8986 100644 --- a/src/handlers/handler_test_utils.rs +++ b/src/handlers/handler_test_utils.rs @@ -99,86 +99,96 @@ mod test_utils { #[macro_export] macro_rules! test_iterable_scroll { - ($func:ident, $handler:ident, $data_ref:ident, $block:expr, $context:expr) => { + ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $block:expr, $context:expr) => { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { let mut app = App::default(); + app.push_navigation_stack($block.into()); app .data - .radarr_data + .$servarr_data .$data_ref .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); $handler::with(&key, &mut app, &$block, &$context).handle(); - assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 2"); + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection(), + "Test 2" + ); $handler::with(&key, &mut app, &$block, &$context).handle(); - assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 1"); + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection(), + "Test 1" + ); } }; - ($func:ident, $handler:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => { + ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { let mut app = App::default(); + app.push_navigation_stack($block.into()); app .data - .radarr_data + .$servarr_data .$data_ref .set_items(simple_stateful_iterable_vec!($items)); - $handler::with(&key, &mut app, &$block, &$context).handle(); + $handler::with(key, &mut app, $block, $context).handle(); - assert_str_eq!( - app.data.radarr_data.$data_ref.current_selection().$field, + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection().$field, "Test 2" ); - $handler::with(&key, &mut app, &$block, &$context).handle(); + $handler::with(key, &mut app, $block, $context).handle(); - assert_str_eq!( - app.data.radarr_data.$data_ref.current_selection().$field, + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection().$field, "Test 1" ); } }; - ($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => { + ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { let mut app = App::default(); - app.data.radarr_data.$data_ref.set_items($items); + app.push_navigation_stack($block.into()); + app.data.$servarr_data.$data_ref.set_items($items); - $handler::with(&key, &mut app, &$block, &$context).handle(); + $handler::with(key, &mut app, $block, $context).handle(); - assert_str_eq!( - app.data.radarr_data.$data_ref.current_selection().$field, + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection().$field, "Test 2" ); - $handler::with(&key, &mut app, &$block, &$context).handle(); + $handler::with(key, &mut app, $block, $context).handle(); - assert_str_eq!( - app.data.radarr_data.$data_ref.current_selection().$field, + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection().$field, "Test 1" ); } }; - ($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => { + ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => { #[rstest] fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) { let mut app = App::default(); - app.data.radarr_data.$data_ref.set_items($items); + app.push_navigation_stack($block.into()); + app.data.$servarr_data.$data_ref.set_items($items); - $handler::with(&key, &mut app, &$block, &$context).handle(); + $handler::with(key, &mut app, $block, $context).handle(); - assert_str_eq!( + pretty_assertions::assert_str_eq!( app .data - .radarr_data + .$servarr_data .$data_ref .current_selection() .$field @@ -186,12 +196,12 @@ mod test_utils { "Test 2" ); - $handler::with(&key, &mut app, &$block, &$context).handle(); + $handler::with(key, &mut app, $block, $context).handle(); - assert_str_eq!( + pretty_assertions::assert_str_eq!( app .data - .radarr_data + .$servarr_data .$data_ref .current_selection() .$field @@ -204,86 +214,96 @@ mod test_utils { #[macro_export] macro_rules! test_iterable_home_and_end { - ($func:ident, $handler:ident, $data_ref:ident, $block:expr, $context:expr) => { + ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $block:expr, $context:expr) => { #[test] fn $func() { let mut app = App::default(); - app.data.radarr_data.$data_ref.set_items(vec![ + app.push_navigation_stack($block.into()); + app.data.$servarr_data.$data_ref.set_items(vec![ "Test 1".to_owned(), "Test 2".to_owned(), "Test 3".to_owned(), ]); - $handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle(); + $handler::with(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle(); - assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 3"); + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection(), + "Test 3" + ); - $handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle(); + $handler::with(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle(); - assert_str_eq!(app.data.radarr_data.$data_ref.current_selection(), "Test 1"); + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection(), + "Test 1" + ); } }; - ($func:ident, $handler:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => { + ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:ident, $block:expr, $context:expr, $field:ident) => { #[test] fn $func() { let mut app = App::default(); + app.push_navigation_stack($block.into()); app .data - .radarr_data + .$servarr_data .$data_ref .set_items(extended_stateful_iterable_vec!($items)); - $handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle(); + $handler::with(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle(); - assert_str_eq!( - app.data.radarr_data.$data_ref.current_selection().$field, + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection().$field, "Test 3" ); - $handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle(); + $handler::with(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle(); - assert_str_eq!( - app.data.radarr_data.$data_ref.current_selection().$field, + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection().$field, "Test 1" ); } }; - ($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => { + ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => { #[test] fn $func() { let mut app = App::default(); - app.data.radarr_data.$data_ref.set_items($items); + app.push_navigation_stack($block.into()); + app.data.$servarr_data.$data_ref.set_items($items); - $handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle(); + $handler::with(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle(); - assert_str_eq!( - app.data.radarr_data.$data_ref.current_selection().$field, + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection().$field, "Test 3" ); - $handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle(); + $handler::with(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle(); - assert_str_eq!( - app.data.radarr_data.$data_ref.current_selection().$field, + pretty_assertions::assert_str_eq!( + app.data.$servarr_data.$data_ref.current_selection().$field, "Test 1" ); } }; - ($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => { + ($func:ident, $handler:ident, $servarr_data:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => { #[test] fn $func() { let mut app = App::default(); - app.data.radarr_data.$data_ref.set_items($items); + app.push_navigation_stack($block.into()); + app.data.$servarr_data.$data_ref.set_items($items); - $handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle(); + $handler::with(DEFAULT_KEYBINDINGS.end.key, &mut app, $block, $context).handle(); - assert_str_eq!( + pretty_assertions::assert_str_eq!( app .data - .radarr_data + .$servarr_data .$data_ref .current_selection() .$field @@ -291,12 +311,12 @@ mod test_utils { "Test 3" ); - $handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle(); + $handler::with(DEFAULT_KEYBINDINGS.home.key, &mut app, $block, $context).handle(); - assert_str_eq!( + pretty_assertions::assert_str_eq!( app .data - .radarr_data + .$servarr_data .$data_ref .current_selection() .$field @@ -311,18 +331,125 @@ mod test_utils { macro_rules! test_handler_delegation { ($handler:ident, $base:expr, $active_block:expr) => { let mut app = App::default(); - app.push_navigation_stack($base.clone().into()); - app.push_navigation_stack($active_block.clone().into()); + app.data.sonarr_data.history.set_items(vec![ + $crate::models::sonarr_models::SonarrHistoryItem::default(), + ]); + app + .data + .sonarr_data + .root_folders + .set_items(vec![$crate::models::servarr_models::RootFolder::default()]); + app + .data + .sonarr_data + .indexers + .set_items(vec![$crate::models::servarr_models::Indexer::default()]); + app + .data + .sonarr_data + .blocklist + .set_items(vec![$crate::models::sonarr_models::BlocklistItem::default()]); + app.data.sonarr_data.add_searched_series = + Some($crate::models::stateful_table::StatefulTable::default()); + app + .data + .radarr_data + .movies + .set_items(vec![$crate::models::radarr_models::Movie::default()]); + app + .data + .radarr_data + .collections + .set_items(vec![$crate::models::radarr_models::Collection::default()]); + app.data.radarr_data.collection_movies.set_items(vec![ + $crate::models::radarr_models::CollectionMovie::default(), + ]); + app + .data + .radarr_data + .indexers + .set_items(vec![$crate::models::servarr_models::Indexer::default()]); + app + .data + .radarr_data + .root_folders + .set_items(vec![$crate::models::servarr_models::RootFolder::default()]); + app + .data + .radarr_data + .blocklist + .set_items(vec![$crate::models::radarr_models::BlocklistItem::default()]); + app.data.radarr_data.add_searched_movies = + Some($crate::models::stateful_table::StatefulTable::default()); + let mut movie_details_modal = + $crate::models::servarr_data::radarr::modals::MovieDetailsModal::default(); + movie_details_modal.movie_history.set_items(vec![ + $crate::models::radarr_models::MovieHistoryItem::default(), + ]); + movie_details_modal + .movie_cast + .set_items(vec![$crate::models::radarr_models::Credit::default()]); + movie_details_modal + .movie_crew + .set_items(vec![$crate::models::radarr_models::Credit::default()]); + movie_details_modal + .movie_releases + .set_items(vec![$crate::models::radarr_models::RadarrRelease::default()]); + app.data.radarr_data.movie_details_modal = Some(movie_details_modal); + let mut season_details_modal = + $crate::models::servarr_data::sonarr::modals::SeasonDetailsModal::default(); + season_details_modal.season_history.set_items(vec![ + $crate::models::sonarr_models::SonarrHistoryItem::default(), + ]); + season_details_modal.episode_details_modal = + Some($crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal::default()); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + let mut series_history = $crate::models::stateful_table::StatefulTable::default(); + series_history.set_items(vec![ + $crate::models::sonarr_models::SonarrHistoryItem::default(), + ]); + app.data.sonarr_data.series_history = Some(series_history); + app + .data + .sonarr_data + .series + .set_items(vec![$crate::models::sonarr_models::Series::default()]); + app.push_navigation_stack($base.into()); + app.push_navigation_stack($active_block.into()); - $handler::with( - &DEFAULT_KEYBINDINGS.esc.key, - &mut app, - &$active_block, - &None, - ) - .handle(); + $handler::with(DEFAULT_KEYBINDINGS.esc.key, &mut app, $active_block, None).handle(); - assert_eq!(app.get_current_route(), &$base.into()); + pretty_assertions::assert_eq!(app.get_current_route(), $base.into()); + }; + } + + #[macro_export] + macro_rules! assert_delete_prompt { + ($handler:ident, $block:expr, $expected_block:expr) => { + let mut app = App::default(); + + $handler::with(DELETE_KEY, &mut app, $block, None).handle(); + + pretty_assertions::assert_eq!(app.get_current_route(), $expected_block.into()); + }; + + ($handler:ident, $app:expr, $block:expr, $expected_block:expr) => { + $handler::with(DELETE_KEY, &mut $app, $block, None).handle(); + + pretty_assertions::assert_eq!($app.get_current_route(), $expected_block.into()); + }; + } + + #[macro_export] + macro_rules! assert_refresh_key { + ($handler:ident, $block:expr) => { + let mut app = App::default(); + app.push_navigation_stack($block.into()); + + $handler::with(DEFAULT_KEYBINDINGS.refresh.key, &mut app, $block, None).handle(); + + pretty_assertions::assert_eq!(app.get_current_route(), $block.into()); + assert!(app.should_refresh); }; } } diff --git a/src/handlers/handlers_tests.rs b/src/handlers/handlers_tests.rs index f203227..0b0fa44 100644 --- a/src/handlers/handlers_tests.rs +++ b/src/handlers/handlers_tests.rs @@ -1,10 +1,19 @@ #[cfg(test)] mod tests { + use crate::models::radarr_models::Movie; + use crate::models::sonarr_models::Series; + use pretty_assertions::assert_eq; use rstest::rstest; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; + use crate::handlers::handle_events; use crate::handlers::{handle_clear_errors, handle_prompt_toggle}; + use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; + use crate::models::HorizontallyScrollableText; + use crate::models::Route; #[test] fn test_handle_clear_errors() { @@ -17,17 +26,87 @@ mod tests { } #[rstest] - fn test_handle_prompt_toggle_left_right(#[values(Key::Left, Key::Right)] key: Key) { + #[case(ActiveRadarrBlock::Movies.into(), ActiveRadarrBlock::SearchMovie.into())] + #[case(ActiveSonarrBlock::Series.into(), ActiveSonarrBlock::SearchSeries.into())] + fn test_handle_events(#[case] base_block: Route, #[case] top_block: Route) { let mut app = App::default(); + app.push_navigation_stack(base_block); + app.push_navigation_stack(top_block); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + handle_events(DEFAULT_KEYBINDINGS.esc.key, &mut app); + + assert_eq!(app.get_current_route(), base_block); + } + + #[rstest] + #[case(0, ActiveSonarrBlock::Series, ActiveSonarrBlock::Series)] + #[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies)] + fn test_handle_change_tabs(#[case] index: usize, #[case] left_block: T, #[case] right_block: T) + where + T: Into + Copy, + { + let mut app = App::default(); + app.error = "Test".into(); + app.server_tabs.set_index(index); + + handle_events(DEFAULT_KEYBINDINGS.previous_servarr.key, &mut app); + + assert_eq!(app.server_tabs.get_active_route(), left_block.into()); + assert_eq!(app.get_current_route(), left_block.into()); + assert!(app.is_first_render); + assert_eq!(app.error, HorizontallyScrollableText::default()); + + app.server_tabs.set_index(index); + app.is_first_render = false; + app.error = "Test".into(); + + handle_events(DEFAULT_KEYBINDINGS.next_servarr.key, &mut app); + + assert_eq!(app.server_tabs.get_active_route(), right_block.into()); + assert_eq!(app.get_current_route(), right_block.into()); + assert!(app.is_first_render); + assert_eq!(app.error, HorizontallyScrollableText::default()); + } + + #[rstest] + fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); assert!(!app.data.radarr_data.prompt_confirm); - handle_prompt_toggle(&mut app, &key); + handle_prompt_toggle(&mut app, key); assert!(app.data.radarr_data.prompt_confirm); - handle_prompt_toggle(&mut app, &key); + handle_prompt_toggle(&mut app, key); assert!(!app.data.radarr_data.prompt_confirm); } + + #[rstest] + fn test_handle_prompt_toggle_left_right_sonarr(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + assert!(!app.data.sonarr_data.prompt_confirm); + + handle_prompt_toggle(&mut app, key); + + assert!(app.data.sonarr_data.prompt_confirm); + + handle_prompt_toggle(&mut app, key); + + assert!(!app.data.sonarr_data.prompt_confirm); + } } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index d7064a1..ed2a906 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,4 +1,5 @@ use radarr_handlers::RadarrHandler; +use sonarr_handlers::SonarrHandler; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; @@ -6,6 +7,7 @@ use crate::event::Key; use crate::models::{HorizontallyScrollableText, Route}; mod radarr_handlers; +mod sonarr_handlers; #[cfg(test)] #[path = "handlers_tests.rs"] @@ -14,45 +16,46 @@ mod handlers_tests; #[cfg(test)] #[path = "handler_test_utils.rs"] pub mod handler_test_utils; +mod table_handler; -pub trait KeyEventHandler<'a, 'b, T: Into> { +pub trait KeyEventHandler<'a, 'b, T: Into + Copy> { fn handle_key_event(&mut self) { let key = self.get_key(); match key { - _ if *key == DEFAULT_KEYBINDINGS.up.key => { + _ if key == DEFAULT_KEYBINDINGS.up.key => { if self.is_ready() { self.handle_scroll_up(); } } - _ if *key == DEFAULT_KEYBINDINGS.down.key => { + _ if key == DEFAULT_KEYBINDINGS.down.key => { if self.is_ready() { self.handle_scroll_down(); } } - _ if *key == DEFAULT_KEYBINDINGS.home.key => { + _ if key == DEFAULT_KEYBINDINGS.home.key => { if self.is_ready() { self.handle_home(); } } - _ if *key == DEFAULT_KEYBINDINGS.end.key => { + _ if key == DEFAULT_KEYBINDINGS.end.key => { if self.is_ready() { self.handle_end(); } } - _ if *key == DEFAULT_KEYBINDINGS.delete.key => { + _ if key == DEFAULT_KEYBINDINGS.delete.key => { if self.is_ready() { self.handle_delete(); } } - _ if *key == DEFAULT_KEYBINDINGS.left.key || *key == DEFAULT_KEYBINDINGS.right.key => { + _ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => { self.handle_left_right_action() } - _ if *key == DEFAULT_KEYBINDINGS.submit.key => { + _ if key == DEFAULT_KEYBINDINGS.submit.key => { if self.is_ready() { self.handle_submit(); } } - _ if *key == DEFAULT_KEYBINDINGS.esc.key => self.handle_esc(), + _ if key == DEFAULT_KEYBINDINGS.esc.key => self.handle_esc(), _ => { if self.is_ready() { self.handle_char_key_event(); @@ -65,9 +68,9 @@ pub trait KeyEventHandler<'a, 'b, T: Into> { self.handle_key_event(); } - fn accepts(active_block: &'a T) -> bool; - fn with(key: &'a Key, app: &'a mut App<'b>, active_block: &'a T, context: &'a Option) -> Self; - fn get_key(&self) -> &Key; + fn accepts(active_block: T) -> bool; + fn with(key: Key, app: &'a mut App<'b>, active_block: T, context: Option) -> Self; + fn get_key(&self) -> Key; fn is_ready(&self) -> bool; fn handle_scroll_up(&mut self); fn handle_scroll_down(&mut self); @@ -81,8 +84,24 @@ pub trait KeyEventHandler<'a, 'b, T: Into> { } pub fn handle_events(key: Key, app: &mut App<'_>) { - if let Route::Radarr(active_radarr_block, context) = *app.get_current_route() { - RadarrHandler::with(&key, app, &active_radarr_block, &context).handle() + if key == DEFAULT_KEYBINDINGS.next_servarr.key { + app.reset(); + app.server_tabs.next(); + app.pop_and_push_navigation_stack(app.server_tabs.get_active_route()); + } else if key == DEFAULT_KEYBINDINGS.previous_servarr.key { + app.reset(); + app.server_tabs.previous(); + app.pop_and_push_navigation_stack(app.server_tabs.get_active_route()); + } else { + match app.get_current_route() { + Route::Radarr(active_radarr_block, context) => { + RadarrHandler::with(key, app, active_radarr_block, context).handle() + } + Route::Sonarr(active_sonarr_block, context) => { + SonarrHandler::with(key, app, active_sonarr_block, context).handle() + } + _ => (), + } } } @@ -92,11 +111,17 @@ fn handle_clear_errors(app: &mut App<'_>) { } } -fn handle_prompt_toggle(app: &mut App<'_>, key: &Key) { +fn handle_prompt_toggle(app: &mut App<'_>, key: Key) { match key { - _ if *key == DEFAULT_KEYBINDINGS.left.key || *key == DEFAULT_KEYBINDINGS.right.key => { - if let Route::Radarr(_, _) = *app.get_current_route() { - app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm; + _ if key == DEFAULT_KEYBINDINGS.left.key || key == DEFAULT_KEYBINDINGS.right.key => { + match app.get_current_route() { + Route::Radarr(_, _) => { + app.data.radarr_data.prompt_confirm = !app.data.radarr_data.prompt_confirm + } + Route::Sonarr(_, _) => { + app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm + } + _ => (), } } _ => (), @@ -107,10 +132,10 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: &Key) { macro_rules! handle_text_box_left_right_keys { ($self:expr, $key:expr, $input:expr) => { match $self.key { - _ if *$key == DEFAULT_KEYBINDINGS.left.key => { + _ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key => { $input.scroll_left(); } - _ if *$key == DEFAULT_KEYBINDINGS.right.key => { + _ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.right.key => { $input.scroll_right(); } _ => (), @@ -122,13 +147,26 @@ macro_rules! handle_text_box_left_right_keys { macro_rules! handle_text_box_keys { ($self:expr, $key:expr, $input:expr) => { match $self.key { - _ if *$key == DEFAULT_KEYBINDINGS.backspace.key => { + _ if $key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.backspace.key => { $input.pop(); } Key::Char(character) => { - $input.push(*character); + $input.push(character); } _ => (), } }; } + +#[macro_export] +macro_rules! handle_prompt_left_right_keys { + ($self:expr, $confirm_prompt:expr, $data:ident) => { + if $self.app.data.$data.selected_block.get_active_block() == $confirm_prompt { + handle_prompt_toggle($self.app, $self.key); + } else if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key { + $self.app.data.$data.selected_block.left(); + } else { + $self.app.data.$data.selected_block.right(); + } + }; +} diff --git a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs index 67d7dbf..2a44f7f 100644 --- a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs @@ -14,246 +14,6 @@ mod tests { use crate::models::radarr_models::{BlocklistItem, BlocklistItemMovie}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::models::servarr_models::{Language, Quality, QualityWrapper}; - use crate::models::stateful_table::SortOption; - - mod test_handle_scroll_up_and_down { - use pretty_assertions::{assert_eq, assert_str_eq}; - use rstest::rstest; - - use crate::models::radarr_models::BlocklistItem; - use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; - - use super::*; - - test_iterable_scroll!( - test_blocklist_scroll, - BlocklistHandler, - blocklist, - simple_stateful_iterable_vec!(BlocklistItem, String, source_title), - ActiveRadarrBlock::Blocklist, - None, - source_title, - to_string - ); - - #[rstest] - fn test_blocklist_scroll_no_op_when_not_ready( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .blocklist - .set_items(simple_stateful_iterable_vec!( - BlocklistItem, - String, - source_title - )); - - BlocklistHandler::with(&key, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .blocklist - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - - BlocklistHandler::with(&key, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .blocklist - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - } - - #[rstest] - fn test_blocklist_sort_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let blocklist_field_vec = sort_options(); - let mut app = App::default(); - app.data.radarr_data.blocklist.sorting(sort_options()); - - if key == Key::Up { - for i in (0..blocklist_field_vec.len()).rev() { - BlocklistHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::BlocklistSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .blocklist - .sort - .as_ref() - .unwrap() - .current_selection(), - &blocklist_field_vec[i] - ); - } - } else { - for i in 0..blocklist_field_vec.len() { - BlocklistHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::BlocklistSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .blocklist - .sort - .as_ref() - .unwrap() - .current_selection(), - &blocklist_field_vec[(i + 1) % blocklist_field_vec.len()] - ); - } - } - } - } - - mod test_handle_home_end { - use pretty_assertions::{assert_eq, assert_str_eq}; - - use crate::models::radarr_models::BlocklistItem; - use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; - - use super::*; - - test_iterable_home_and_end!( - test_blocklist_home_and_end, - BlocklistHandler, - blocklist, - extended_stateful_iterable_vec!(BlocklistItem, String, source_title), - ActiveRadarrBlock::Blocklist, - None, - source_title, - to_string - ); - - #[test] - fn test_blocklist_home_and_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .blocklist - .set_items(extended_stateful_iterable_vec!( - BlocklistItem, - String, - source_title - )); - - BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Blocklist, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .blocklist - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - - BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Blocklist, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .blocklist - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - } - - #[test] - fn test_blocklist_sort_home_end() { - let blocklist_field_vec = sort_options(); - let mut app = App::default(); - app.data.radarr_data.blocklist.sorting(sort_options()); - - BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::BlocklistSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .blocklist - .sort - .as_ref() - .unwrap() - .current_selection(), - &blocklist_field_vec[blocklist_field_vec.len() - 1] - ); - - BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::BlocklistSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .blocklist - .sort - .as_ref() - .unwrap() - .current_selection(), - &blocklist_field_vec[0] - ); - } - } mod test_handle_delete { use pretty_assertions::assert_eq; @@ -267,11 +27,11 @@ mod tests { let mut app = App::default(); app.data.radarr_data.blocklist.set_items(blocklist_vec()); - BlocklistHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); + BlocklistHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::DeleteBlocklistItemPrompt.into() + ActiveRadarrBlock::DeleteBlocklistItemPrompt.into() ); } @@ -282,12 +42,9 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.data.radarr_data.blocklist.set_items(blocklist_vec()); - BlocklistHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); + BlocklistHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); } } @@ -304,21 +61,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(3); BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::Blocklist, - &None, + ActiveRadarrBlock::Blocklist, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Downloads.into() - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() + ActiveRadarrBlock::Downloads.into() ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into()); } #[rstest] @@ -328,20 +82,20 @@ mod tests { app.data.radarr_data.main_tabs.set_index(3); BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::Blocklist, - &None, + ActiveRadarrBlock::Blocklist, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); } @@ -356,11 +110,11 @@ mod tests { ) { let mut app = App::default(); - BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); + BlocklistHandler::with(key, &mut app, active_radarr_block, None).handle(); assert!(app.data.radarr_data.prompt_confirm); - BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); + BlocklistHandler::with(key, &mut app, active_radarr_block, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); } @@ -382,11 +136,11 @@ mod tests { app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); - BlocklistHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); + BlocklistHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::BlocklistItemDetails.into() + ActiveRadarrBlock::BlocklistItemDetails.into() ); } @@ -397,12 +151,9 @@ mod tests { app.data.radarr_data.blocklist.set_items(blocklist_vec()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); - BlocklistHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); + BlocklistHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); } #[rstest] @@ -427,14 +178,14 @@ mod tests { app.push_navigation_stack(base_route.into()); app.push_navigation_stack(prompt_block.into()); - BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); + BlocklistHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(expected_action) ); - assert_eq!(app.get_current_route(), &base_route.into()); + assert_eq!(app.get_current_route(), base_route.into()); } #[rstest] @@ -450,42 +201,11 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(prompt_block.into()); - BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); + BlocklistHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); - } - - #[test] - fn test_blocklist_sort_prompt_submit() { - let mut app = App::default(); - app.data.radarr_data.blocklist.sort_asc = true; - app.data.radarr_data.blocklist.sorting(sort_options()); - app.data.radarr_data.blocklist.set_items(blocklist_vec()); - app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); - app.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into()); - - let mut expected_vec = blocklist_vec(); - expected_vec.sort_by(|a, b| a.id.cmp(&b.id)); - expected_vec.reverse(); - - BlocklistHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::BlocklistSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); - assert_eq!(app.data.radarr_data.blocklist.items, expected_vec); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); } } @@ -517,9 +237,9 @@ mod tests { app.push_navigation_stack(prompt_block.into()); app.data.radarr_data.prompt_confirm = true; - BlocklistHandler::with(&ESC_KEY, &mut app, &prompt_block, &None).handle(); + BlocklistHandler::with(ESC_KEY, &mut app, prompt_block, None).handle(); - assert_eq!(app.get_current_route(), &base_block.into()); + assert_eq!(app.get_current_route(), base_block.into()); assert!(!app.data.radarr_data.prompt_confirm); } @@ -530,37 +250,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::BlocklistItemDetails.into()); BlocklistHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::BlocklistItemDetails, - &None, + ActiveRadarrBlock::BlocklistItemDetails, + None, ) .handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); - } - - #[test] - fn test_blocklist_sort_prompt_block_esc() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); - app.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into()); - - BlocklistHandler::with( - &ESC_KEY, - &mut app, - &ActiveRadarrBlock::BlocklistSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); } #[rstest] @@ -571,12 +268,9 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); - DownloadsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); + DownloadsHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::Blocklist, None).handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); assert!(app.error.text.is_empty()); } } @@ -596,17 +290,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Blocklist, - &None, + ActiveRadarrBlock::Blocklist, + None, ) .handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); assert!(app.should_refresh); } @@ -618,17 +309,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Blocklist, - &None, + ActiveRadarrBlock::Blocklist, + None, ) .handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); assert!(!app.should_refresh); } @@ -638,16 +326,16 @@ mod tests { app.data.radarr_data.blocklist.set_items(blocklist_vec()); BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.clear.key, + DEFAULT_KEYBINDINGS.clear.key, &mut app, - &ActiveRadarrBlock::Blocklist, - &None, + ActiveRadarrBlock::Blocklist, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into() + ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into() ); } @@ -659,64 +347,14 @@ mod tests { app.data.radarr_data.blocklist.set_items(blocklist_vec()); BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.clear.key, + DEFAULT_KEYBINDINGS.clear.key, &mut app, - &ActiveRadarrBlock::Blocklist, - &None, + ActiveRadarrBlock::Blocklist, + None, ) .handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); - } - - #[test] - fn test_sort_key() { - let mut app = App::default(); - app.data.radarr_data.blocklist.set_items(blocklist_vec()); - - BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.sort.key, - &mut app, - &ActiveRadarrBlock::Blocklist, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::BlocklistSortPrompt.into() - ); - assert_eq!( - app.data.radarr_data.blocklist.sort.as_ref().unwrap().items, - blocklist_sorting_options() - ); - assert!(!app.data.radarr_data.blocklist.sort_asc); - } - - #[test] - fn test_sort_key_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); - app.data.radarr_data.blocklist.set_items(blocklist_vec()); - - BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.sort.key, - &mut app, - &ActiveRadarrBlock::Blocklist, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() - ); - assert!(app.data.radarr_data.blocklist.sort.is_none()); - assert!(!app.data.radarr_data.blocklist.sort_asc); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); } #[rstest] @@ -741,10 +379,10 @@ mod tests { app.push_navigation_stack(prompt_block.into()); BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &prompt_block, - &None, + prompt_block, + None, ) .handle(); @@ -753,7 +391,7 @@ mod tests { app.data.radarr_data.prompt_confirm_action, Some(expected_action) ); - assert_eq!(app.get_current_route(), &base_route.into()); + assert_eq!(app.get_current_route(), base_route.into()); } } @@ -896,9 +534,9 @@ mod tests { fn test_blocklist_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if BLOCKLIST_BLOCKS.contains(&active_radarr_block) { - assert!(BlocklistHandler::accepts(&active_radarr_block)); + assert!(BlocklistHandler::accepts(active_radarr_block)); } else { - assert!(!BlocklistHandler::accepts(&active_radarr_block)); + assert!(!BlocklistHandler::accepts(active_radarr_block)); } }) } @@ -909,10 +547,10 @@ mod tests { app.is_loading = true; let handler = BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Blocklist, - &None, + ActiveRadarrBlock::Blocklist, + None, ); assert!(!handler.is_ready()); @@ -924,10 +562,10 @@ mod tests { app.is_loading = false; let handler = BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Blocklist, - &None, + ActiveRadarrBlock::Blocklist, + None, ); assert!(!handler.is_ready()); @@ -944,10 +582,10 @@ mod tests { .set_items(vec![BlocklistItem::default()]); let handler = BlocklistHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Blocklist, - &None, + ActiveRadarrBlock::Blocklist, + None, ); assert!(handler.is_ready()); @@ -1029,15 +667,4 @@ mod tests { }, ] } - - fn sort_options() -> Vec> { - vec![SortOption { - name: "Test 1", - cmp_fn: Some(|a, b| { - b.source_title - .to_lowercase() - .cmp(&a.source_title.to_lowercase()) - }), - }] - } } diff --git a/src/handlers/radarr_handlers/blocklist/mod.rs b/src/handlers/radarr_handlers/blocklist/mod.rs index 88bc21f..2f23683 100644 --- a/src/handlers/radarr_handlers/blocklist/mod.rs +++ b/src/handlers/radarr_handlers/blocklist/mod.rs @@ -1,12 +1,13 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; +use crate::handle_table_events; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::radarr_models::BlocklistItem; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; use crate::models::stateful_table::SortOption; -use crate::models::Scrollable; use crate::network::radarr_network::RadarrEvent; #[cfg(test)] @@ -14,22 +15,43 @@ use crate::network::radarr_network::RadarrEvent; mod blocklist_handler_tests; pub(super) struct BlocklistHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, +} + +impl<'a, 'b> BlocklistHandler<'a, 'b> { + handle_table_events!( + self, + blocklist, + self.app.data.radarr_data.blocklist, + BlocklistItem + ); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - BLOCKLIST_BLOCKS.contains(active_block) + fn handle(&mut self) { + let blocklist_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::Blocklist.into()) + .sorting_block(ActiveRadarrBlock::BlocklistSortPrompt.into()) + .sort_by_fn(|a: &BlocklistItem, b: &BlocklistItem| a.id.cmp(&b.id)) + .sort_options(blocklist_sorting_options()); + + if !self.handle_blocklist_table_events(blocklist_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveRadarrBlock) -> bool { + BLOCKLIST_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> Self { BlocklistHandler { key, @@ -39,7 +61,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -47,72 +69,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, !self.app.is_loading && !self.app.data.radarr_data.blocklist.is_empty() } - fn handle_scroll_up(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_up(), - ActiveRadarrBlock::BlocklistSortPrompt => self - .app - .data - .radarr_data - .blocklist - .sort - .as_mut() - .unwrap() - .scroll_up(), - _ => (), - } - } + fn handle_scroll_up(&mut self) {} - fn handle_scroll_down(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_down(), - ActiveRadarrBlock::BlocklistSortPrompt => self - .app - .data - .radarr_data - .blocklist - .sort - .as_mut() - .unwrap() - .scroll_down(), - _ => (), - } - } + fn handle_scroll_down(&mut self) {} - fn handle_home(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_to_top(), - ActiveRadarrBlock::BlocklistSortPrompt => self - .app - .data - .radarr_data - .blocklist - .sort - .as_mut() - .unwrap() - .scroll_to_top(), - _ => (), - } - } + fn handle_home(&mut self) {} - fn handle_end(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_to_bottom(), - ActiveRadarrBlock::BlocklistSortPrompt => self - .app - .data - .radarr_data - .blocklist - .sort - .as_mut() - .unwrap() - .scroll_to_bottom(), - _ => (), - } - } + fn handle_end(&mut self) {} fn handle_delete(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Blocklist { + if self.active_radarr_block == ActiveRadarrBlock::Blocklist { self .app .push_navigation_stack(ActiveRadarrBlock::DeleteBlocklistItemPrompt.into()); @@ -145,18 +111,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, self.app.pop_navigation_stack(); } - ActiveRadarrBlock::BlocklistSortPrompt => { - self - .app - .data - .radarr_data - .blocklist - .items - .sort_by(|a, b| a.id.cmp(&b.id)); - self.app.data.radarr_data.blocklist.apply_sorting(); - - self.app.pop_navigation_stack(); - } ActiveRadarrBlock::Blocklist => { self .app @@ -173,7 +127,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, self.app.pop_navigation_stack(); self.app.data.radarr_data.prompt_confirm = false; } - ActiveRadarrBlock::BlocklistItemDetails | ActiveRadarrBlock::BlocklistSortPrompt => { + ActiveRadarrBlock::BlocklistItemDetails => { self.app.pop_navigation_stack(); } _ => handle_clear_errors(self.app), @@ -184,29 +138,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, let key = self.key; match self.active_radarr_block { ActiveRadarrBlock::Blocklist => match self.key { - _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } - _ if *key == DEFAULT_KEYBINDINGS.clear.key => { + _ if key == DEFAULT_KEYBINDINGS.clear.key => { self .app .push_navigation_stack(ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into()); } - _ if *key == DEFAULT_KEYBINDINGS.sort.key => { - self - .app - .data - .radarr_data - .blocklist - .sorting(blocklist_sorting_options()); - self - .app - .push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into()); - } _ => (), }, ActiveRadarrBlock::DeleteBlocklistItemPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { + if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem(None)); @@ -215,7 +158,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, } } ActiveRadarrBlock::BlocklistClearAllItemsPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { + if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist); diff --git a/src/handlers/radarr_handlers/collections/collection_details_handler.rs b/src/handlers/radarr_handlers/collections/collection_details_handler.rs index c9243d9..1d13768 100644 --- a/src/handlers/radarr_handlers/collections/collection_details_handler.rs +++ b/src/handlers/radarr_handlers/collections/collection_details_handler.rs @@ -1,35 +1,56 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::KeyEventHandler; +use crate::models::radarr_models::CollectionMovie; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, ADD_MOVIE_SELECTION_BLOCKS, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, }; use crate::models::stateful_table::StatefulTable; -use crate::models::{BlockSelectionState, Scrollable}; +use crate::models::BlockSelectionState; #[cfg(test)] #[path = "collection_details_handler_tests.rs"] mod collection_details_handler_tests; pub(super) struct CollectionDetailsHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, +} + +impl<'a, 'b> CollectionDetailsHandler<'a, 'b> { + handle_table_events!( + self, + collection_movies, + self.app.data.radarr_data.collection_movies, + CollectionMovie + ); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - COLLECTION_DETAILS_BLOCKS.contains(active_block) + fn handle(&mut self) { + let collection_movies_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::CollectionDetails.into()); + + if !self.handle_collection_movies_table_events(collection_movies_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveRadarrBlock) -> bool { + COLLECTION_DETAILS_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_block: ActiveRadarrBlock, + _context: Option, ) -> CollectionDetailsHandler<'a, 'b> { CollectionDetailsHandler { key, @@ -39,7 +60,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -47,41 +68,20 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan !self.app.is_loading && !self.app.data.radarr_data.collection_movies.is_empty() } - fn handle_scroll_up(&mut self) { - if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block { - self.app.data.radarr_data.collection_movies.scroll_up() - } - } + fn handle_scroll_up(&mut self) {} - fn handle_scroll_down(&mut self) { - if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block { - self.app.data.radarr_data.collection_movies.scroll_down() - } - } + fn handle_scroll_down(&mut self) {} - fn handle_home(&mut self) { - if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block { - self.app.data.radarr_data.collection_movies.scroll_to_top(); - } - } + fn handle_home(&mut self) {} - fn handle_end(&mut self) { - if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block { - self - .app - .data - .radarr_data - .collection_movies - .scroll_to_bottom(); - } - } + fn handle_end(&mut self) {} fn handle_delete(&mut self) {} fn handle_left_right_action(&mut self) {} fn handle_submit(&mut self) { - if ActiveRadarrBlock::CollectionDetails == *self.active_radarr_block { + if ActiveRadarrBlock::CollectionDetails == self.active_radarr_block { let tmdb_id = self .app .data @@ -111,7 +111,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan .into(), ); self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); + BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS); self.app.data.radarr_data.add_movie_modal = Some((&self.app.data.radarr_data).into()); } } @@ -129,19 +129,19 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionDetailsHan } fn handle_char_key_event(&mut self) { - if *self.active_radarr_block == ActiveRadarrBlock::CollectionDetails - && *self.key == DEFAULT_KEYBINDINGS.edit.key + if self.active_radarr_block == ActiveRadarrBlock::CollectionDetails + && self.key == DEFAULT_KEYBINDINGS.edit.key { self.app.push_navigation_stack( ( ActiveRadarrBlock::EditCollectionPrompt, - Some(*self.active_radarr_block), + Some(self.active_radarr_block), ) .into(), ); self.app.data.radarr_data.edit_collection_modal = Some((&self.app.data.radarr_data).into()); self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); } } } diff --git a/src/handlers/radarr_handlers/collections/collection_details_handler_tests.rs b/src/handlers/radarr_handlers/collections/collection_details_handler_tests.rs index 34659d5..c23a644 100644 --- a/src/handlers/radarr_handlers/collections/collection_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/collection_details_handler_tests.rs @@ -12,142 +12,6 @@ mod tests { use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, }; - use crate::models::HorizontallyScrollableText; - - mod test_handle_scroll_up_and_down { - use rstest::rstest; - - use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; - - use super::*; - - test_iterable_scroll!( - test_collection_details_scroll, - CollectionDetailsHandler, - collection_movies, - simple_stateful_iterable_vec!(CollectionMovie, HorizontallyScrollableText), - ActiveRadarrBlock::CollectionDetails, - None, - title, - to_string - ); - - #[rstest] - fn test_collection_details_scroll_no_op_when_not_ready( - #[values( - DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key - )] - key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .collection_movies - .set_items(simple_stateful_iterable_vec!( - CollectionMovie, - HorizontallyScrollableText - )); - - CollectionDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::CollectionDetails, &None) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collection_movies - .current_selection() - .title - .to_string(), - "Test 1" - ); - - CollectionDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::CollectionDetails, &None) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collection_movies - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - } - - mod test_handle_home_end { - use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; - - use super::*; - - test_iterable_home_and_end!( - test_collection_details_home_end, - CollectionDetailsHandler, - collection_movies, - extended_stateful_iterable_vec!(CollectionMovie, HorizontallyScrollableText), - ActiveRadarrBlock::CollectionDetails, - None, - title, - to_string - ); - - #[test] - fn test_collection_details_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .collection_movies - .set_items(extended_stateful_iterable_vec!( - CollectionMovie, - HorizontallyScrollableText - )); - - CollectionDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collection_movies - .current_selection() - .title - .to_string(), - "Test 1" - ); - - CollectionDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collection_movies - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - } mod test_handle_submit { use bimap::BiMap; @@ -171,24 +35,24 @@ mod tests { .set_items(vec![CollectionMovie::default()]); app.data.radarr_data.quality_profile_map = BiMap::from_iter([(1, "B - Test 2".to_owned()), (0, "A - Test 1".to_owned())]); - app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(ADD_MOVIE_SELECTION_BLOCKS.len() - 1); + .set_index(0, ADD_MOVIE_SELECTION_BLOCKS.len() - 1); CollectionDetailsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, + ActiveRadarrBlock::CollectionDetails, + None, ) .handle(); assert_eq!( app.get_current_route(), - &( + ( ActiveRadarrBlock::AddMoviePrompt, Some(ActiveRadarrBlock::CollectionDetails) ) @@ -205,7 +69,7 @@ mod tests { .is_empty()); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::AddMovieSelectRootFolder + ActiveRadarrBlock::AddMovieSelectRootFolder ); assert!(!app .data @@ -250,16 +114,16 @@ mod tests { .set_items(vec![CollectionMovie::default()]); CollectionDetailsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, + ActiveRadarrBlock::CollectionDetails, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::CollectionDetails.into() + ActiveRadarrBlock::CollectionDetails.into() ); assert!(app.data.radarr_data.add_movie_modal.is_none()); } @@ -279,16 +143,16 @@ mod tests { .set_items(vec![Movie::default()]); CollectionDetailsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, + ActiveRadarrBlock::CollectionDetails, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::ViewMovieOverview.into() + ActiveRadarrBlock::ViewMovieOverview.into() ); } } @@ -313,16 +177,16 @@ mod tests { .set_items(vec![CollectionMovie::default()]); CollectionDetailsHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, + ActiveRadarrBlock::CollectionDetails, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert!(app.data.radarr_data.collection_movies.items.is_empty()); } @@ -334,16 +198,16 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::ViewMovieOverview.into()); CollectionDetailsHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::ViewMovieOverview, - &None, + ActiveRadarrBlock::ViewMovieOverview, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::CollectionDetails.into() + ActiveRadarrBlock::CollectionDetails.into() ); } } @@ -367,7 +231,7 @@ mod tests { test_edit_collection_key!( CollectionDetailsHandler, ActiveRadarrBlock::CollectionDetails, - ActiveRadarrBlock::CollectionDetails + Some(ActiveRadarrBlock::CollectionDetails) ); } @@ -388,16 +252,16 @@ mod tests { app.data.radarr_data = radarr_data; CollectionDetailsHandler::with( - &DEFAULT_KEYBINDINGS.edit.key, + DEFAULT_KEYBINDINGS.edit.key, &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, + ActiveRadarrBlock::CollectionDetails, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::CollectionDetails.into() + ActiveRadarrBlock::CollectionDetails.into() ); assert!(app.data.radarr_data.edit_collection_modal.is_none()); } @@ -407,9 +271,9 @@ mod tests { fn test_collection_details_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block) { - assert!(CollectionDetailsHandler::accepts(&active_radarr_block)); + assert!(CollectionDetailsHandler::accepts(active_radarr_block)); } else { - assert!(!CollectionDetailsHandler::accepts(&active_radarr_block)); + assert!(!CollectionDetailsHandler::accepts(active_radarr_block)); } }); } @@ -420,10 +284,10 @@ mod tests { app.is_loading = true; let handler = CollectionDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, + ActiveRadarrBlock::CollectionDetails, + None, ); assert!(!handler.is_ready()); @@ -435,10 +299,10 @@ mod tests { app.is_loading = false; let handler = CollectionDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, + ActiveRadarrBlock::CollectionDetails, + None, ); assert!(!handler.is_ready()); @@ -455,10 +319,10 @@ mod tests { .set_items(vec![CollectionMovie::default()]); let handler = CollectionDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::CollectionDetails, - &None, + ActiveRadarrBlock::CollectionDetails, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs index 1eb268b..a5d72ae 100644 --- a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod tests { - use core::sync::atomic::Ordering::SeqCst; use std::cmp::Ordering; use std::iter; @@ -19,353 +18,7 @@ mod tests { use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTIONS_BLOCKS, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_BLOCKS, }; - use crate::models::stateful_table::SortOption; - use crate::models::HorizontallyScrollableText; - use crate::{extended_stateful_iterable_vec, test_handler_delegation}; - - mod test_handle_scroll_up_and_down { - use pretty_assertions::assert_eq; - use rstest::rstest; - - use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; - - use super::*; - - test_iterable_scroll!( - test_collections_scroll, - CollectionsHandler, - collections, - simple_stateful_iterable_vec!(Collection, HorizontallyScrollableText), - ActiveRadarrBlock::Collections, - None, - title, - to_string - ); - - #[rstest] - fn test_collections_scroll_no_op_when_not_ready( - #[values( - DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key - )] - key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .collections - .set_items(simple_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - - CollectionsHandler::with(&key, &mut app, &ActiveRadarrBlock::Collections, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .current_selection() - .title - .to_string(), - "Test 1" - ); - - CollectionsHandler::with(&key, &mut app, &ActiveRadarrBlock::Collections, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[rstest] - fn test_collections_sort_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let collection_field_vec = sort_options(); - let mut app = App::default(); - app.data.radarr_data.collections.sorting(sort_options()); - - if key == Key::Up { - for i in (0..collection_field_vec.len()).rev() { - CollectionsHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::CollectionsSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .sort - .as_ref() - .unwrap() - .current_selection(), - &collection_field_vec[i] - ); - } - } else { - for i in 0..collection_field_vec.len() { - CollectionsHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::CollectionsSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .sort - .as_ref() - .unwrap() - .current_selection(), - &collection_field_vec[(i + 1) % collection_field_vec.len()] - ); - } - } - } - } - - mod test_handle_home_end { - use pretty_assertions::assert_eq; - - use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; - - use super::*; - - test_iterable_home_and_end!( - test_collections_home_end, - CollectionsHandler, - collections, - extended_stateful_iterable_vec!(Collection, HorizontallyScrollableText), - ActiveRadarrBlock::Collections, - None, - title, - to_string - ); - - #[test] - fn test_collections_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .collections - .set_items(extended_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .current_selection() - .title - .to_string(), - "Test 1" - ); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[test] - fn test_collection_search_box_home_end_keys() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - app.data.radarr_data.collections.search = Some("Test".into()); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::SearchCollection, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .search - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 4 - ); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::SearchCollection, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .search - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 0 - ); - } - - #[test] - fn test_collection_filter_box_home_end_keys() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - app.data.radarr_data.collections.filter = Some("Test".into()); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::FilterCollections, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .filter - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 4 - ); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::FilterCollections, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .filter - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 0 - ); - } - - #[test] - fn test_collections_sort_home_end() { - let collection_field_vec = sort_options(); - let mut app = App::default(); - app.data.radarr_data.collections.sorting(sort_options()); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::CollectionsSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .sort - .as_ref() - .unwrap() - .current_selection(), - &collection_field_vec[collection_field_vec.len() - 1] - ); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::CollectionsSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .sort - .as_ref() - .unwrap() - .current_selection(), - &collection_field_vec[0] - ); - } - } + use crate::test_handler_delegation; mod test_handle_left_right_action { use pretty_assertions::assert_eq; @@ -380,18 +33,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(1); CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Movies.into() + ActiveRadarrBlock::Movies.into() ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } #[rstest] @@ -401,21 +54,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(1); CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Downloads.into() - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() + ActiveRadarrBlock::Downloads.into() ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into()); } #[rstest] @@ -425,121 +75,25 @@ mod tests { let mut app = App::default(); CollectionsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::UpdateAllCollectionsPrompt, - &None, + ActiveRadarrBlock::UpdateAllCollectionsPrompt, + None, ) .handle(); assert!(app.data.radarr_data.prompt_confirm); CollectionsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::UpdateAllCollectionsPrompt, - &None, + ActiveRadarrBlock::UpdateAllCollectionsPrompt, + None, ) .handle(); assert!(!app.data.radarr_data.prompt_confirm); } - - #[test] - fn test_collection_search_box_left_right_keys() { - let mut app = App::default(); - app.data.radarr_data.collections.search = Some("Test".into()); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.left.key, - &mut app, - &ActiveRadarrBlock::SearchCollection, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .search - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 1 - ); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, - &mut app, - &ActiveRadarrBlock::SearchCollection, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .search - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 0 - ); - } - - #[test] - fn test_collection_filter_box_left_right_keys() { - let mut app = App::default(); - app.data.radarr_data.collections.filter = Some("Test".into()); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.left.key, - &mut app, - &ActiveRadarrBlock::FilterCollections, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .filter - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 1 - ); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, - &mut app, - &ActiveRadarrBlock::FilterCollections, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .collections - .filter - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 0 - ); - } } mod test_handle_submit { @@ -560,17 +114,11 @@ mod tests { .collections .set_items(vec![Collection::default()]); - CollectionsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); + CollectionsHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Collections, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::CollectionDetails.into() + ActiveRadarrBlock::CollectionDetails.into() ); } @@ -585,217 +133,11 @@ mod tests { .collections .set_items(vec![Collection::default()]); - CollectionsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); + CollectionsHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Collections, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - } - - #[test] - fn test_search_collections_submit() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.push_navigation_stack(ActiveRadarrBlock::SearchCollection.into()); - app - .data - .radarr_data - .collections - .set_items(extended_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - app.data.radarr_data.collections.search = Some("Test 2".into()); - - CollectionsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::SearchCollection, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .current_selection() - .title - .text, - "Test 2" - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - } - - #[test] - fn test_search_collections_submit_error_on_no_search_hits() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.push_navigation_stack(ActiveRadarrBlock::SearchCollection.into()); - app - .data - .radarr_data - .collections - .set_items(extended_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - app.data.radarr_data.collections.search = Some("Test 5".into()); - - CollectionsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::SearchCollection, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .current_selection() - .title - .text, - "Test 1" - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::SearchCollectionError.into() - ); - } - - #[test] - fn test_search_filtered_collections_submit() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.push_navigation_stack(ActiveRadarrBlock::SearchCollection.into()); - app - .data - .radarr_data - .collections - .set_items(extended_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - app.data.radarr_data.collections.search = Some("Test 2".into()); - - CollectionsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::SearchCollection, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .current_selection() - .title - .text, - "Test 2" - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - } - - #[test] - fn test_filter_collections_submit() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.push_navigation_stack(ActiveRadarrBlock::FilterCollections.into()); - app - .data - .radarr_data - .collections - .set_items(extended_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - app.data.radarr_data.collections.filter = Some("Test".into()); - - CollectionsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::FilterCollections, - &None, - ) - .handle(); - - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.collections.filtered_items.is_some()); - assert_eq!( - app - .data - .radarr_data - .collections - .filtered_items - .as_ref() - .unwrap() - .len(), - 3 - ); - assert_str_eq!( - app - .data - .radarr_data - .collections - .current_selection() - .title - .text, - "Test 1" - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - } - - #[test] - fn test_filter_collections_submit_error_on_no_filter_matches() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.push_navigation_stack(ActiveRadarrBlock::FilterCollections.into()); - app - .data - .radarr_data - .collections - .set_items(extended_stateful_iterable_vec!( - Collection, - HorizontallyScrollableText - )); - app.data.radarr_data.collections.filter = Some("Test 5".into()); - - CollectionsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::FilterCollections, - &None, - ) - .handle(); - - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.collections.filtered_items.is_none()); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::FilterCollectionsError.into() + ActiveRadarrBlock::Collections.into() ); } @@ -812,10 +154,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into()); CollectionsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::UpdateAllCollectionsPrompt, - &None, + ActiveRadarrBlock::UpdateAllCollectionsPrompt, + None, ) .handle(); @@ -826,7 +168,7 @@ mod tests { ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); } @@ -842,10 +184,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into()); CollectionsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::UpdateAllCollectionsPrompt, - &None, + ActiveRadarrBlock::UpdateAllCollectionsPrompt, + None, ) .handle(); @@ -853,111 +195,20 @@ mod tests { assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); } - - #[test] - fn test_collections_sort_prompt_submit() { - let mut app = App::default(); - app.data.radarr_data.collections.sort_asc = true; - app.data.radarr_data.collections.sorting(sort_options()); - app - .data - .radarr_data - .collections - .set_items(collections_vec()); - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into()); - - let mut expected_vec = collections_vec(); - expected_vec.sort_by(|a, b| a.id.cmp(&b.id)); - expected_vec.reverse(); - - CollectionsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::CollectionsSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - assert_eq!(app.data.radarr_data.collections.items, expected_vec); - } } mod test_handle_esc { use pretty_assertions::assert_eq; - use ratatui::widgets::TableState; use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; - use crate::models::stateful_table::StatefulTable; use super::*; const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; - #[rstest] - fn test_search_collection_block_esc( - #[values( - ActiveRadarrBlock::SearchCollection, - ActiveRadarrBlock::SearchCollectionError - )] - active_radarr_block: ActiveRadarrBlock, - ) { - let mut app = App::default(); - app.should_ignore_quit_key = true; - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.push_navigation_stack(active_radarr_block.into()); - app.data.radarr_data = create_test_radarr_data(); - app.data.radarr_data.collections.search = Some("Test".into()); - - CollectionsHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - assert!(!app.should_ignore_quit_key); - assert_eq!(app.data.radarr_data.collections.search, None); - } - - #[rstest] - fn test_filter_collections_block_esc( - #[values( - ActiveRadarrBlock::FilterCollections, - ActiveRadarrBlock::FilterCollectionsError - )] - active_radarr_block: ActiveRadarrBlock, - ) { - let mut app = App::default(); - app.should_ignore_quit_key = true; - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.push_navigation_stack(active_radarr_block.into()); - app.data.radarr_data = create_test_radarr_data(); - app.data.radarr_data.collections = StatefulTable { - filter: Some("Test".into()), - filtered_items: Some(Vec::new()), - filtered_state: Some(TableState::default()), - ..StatefulTable::default() - }; - - CollectionsHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - assert!(!app.should_ignore_quit_key); - assert_eq!(app.data.radarr_data.collections.filter, None); - assert_eq!(app.data.radarr_data.collections.filtered_items, None); - assert_eq!(app.data.radarr_data.collections.filtered_state, None); - } - #[test] fn test_update_all_collections_prompt_block_esc() { let mut app = App::default(); @@ -966,40 +217,20 @@ mod tests { app.data.radarr_data.prompt_confirm = true; CollectionsHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::UpdateAllCollectionsPrompt, - &None, + ActiveRadarrBlock::UpdateAllCollectionsPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert!(!app.data.radarr_data.prompt_confirm); } - #[test] - fn test_collections_sort_prompt_block_esc() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into()); - - CollectionsHandler::with( - &ESC_KEY, - &mut app, - &ActiveRadarrBlock::CollectionsSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - } - #[rstest] fn test_default_esc(#[values(true, false)] is_ready: bool) { let mut app = App::default(); @@ -1008,25 +239,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.data.radarr_data = create_test_radarr_data(); - app.data.radarr_data.collections = StatefulTable { - search: Some("Test".into()), - filter: Some("Test".into()), - filtered_items: Some(Vec::new()), - filtered_state: Some(TableState::default()), - ..StatefulTable::default() - }; - CollectionsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Collections, &None).handle(); + CollectionsHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::Collections, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert!(app.error.text.is_empty()); - assert_eq!(app.data.radarr_data.collections.search, None); - assert_eq!(app.data.radarr_data.collections.filter, None); - assert_eq!(app.data.radarr_data.collections.filtered_items, None); - assert_eq!(app.data.radarr_data.collections.filtered_state, None); } } @@ -1045,154 +265,9 @@ mod tests { use super::*; - #[test] - fn test_search_collections_key() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.search.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::SearchCollection.into() - ); - assert!(app.should_ignore_quit_key); - assert_eq!( - app.data.radarr_data.collections.search, - Some(HorizontallyScrollableText::default()) - ); - } - - #[test] - fn test_search_collections_key_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.search.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - assert!(!app.should_ignore_quit_key); - assert_eq!(app.data.radarr_data.collections.search, None); - } - - #[test] - fn test_filter_collections_key() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.filter.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::FilterCollections.into() - ); - assert!(app.should_ignore_quit_key); - assert!(app.data.radarr_data.collections.filter.is_some()); - } - - #[test] - fn test_filter_collections_key_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.filter.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.collections.filter.is_none()); - } - - #[test] - fn test_filter_collections_key_resets_previous_filter() { - let mut app = App::default(); - app.data.radarr_data = create_test_radarr_data(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - app.should_ignore_quit_key = true; - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app.data.radarr_data.collections.filter = Some("Test".into()); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.filter.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::FilterCollections.into() - ); - assert!(app.should_ignore_quit_key); - assert_eq!( - app.data.radarr_data.collections.filter, - Some(HorizontallyScrollableText::default()) - ); - assert!(app.data.radarr_data.collections.filtered_items.is_none()); - assert!(app.data.radarr_data.collections.filtered_state.is_none()); - } - #[test] fn test_collection_edit_key() { - test_edit_collection_key!( - CollectionsHandler, - ActiveRadarrBlock::Collections, - ActiveRadarrBlock::Collections - ); + test_edit_collection_key!(CollectionsHandler, ActiveRadarrBlock::Collections, None); } #[test] @@ -1212,16 +287,16 @@ mod tests { app.data.radarr_data = radarr_data; CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.edit.key, + DEFAULT_KEYBINDINGS.edit.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert!(app.data.radarr_data.edit_collection_modal.is_none()); } @@ -1236,16 +311,16 @@ mod tests { .set_items(vec![Collection::default()]); CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::UpdateAllCollectionsPrompt.into() + ActiveRadarrBlock::UpdateAllCollectionsPrompt.into() ); } @@ -1261,16 +336,16 @@ mod tests { .set_items(vec![Collection::default()]); CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); } @@ -1285,16 +360,16 @@ mod tests { .set_items(vec![Collection::default()]); CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert!(app.should_refresh); } @@ -1311,206 +386,20 @@ mod tests { .set_items(vec![Collection::default()]); CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert!(!app.should_refresh); } - #[test] - fn test_search_collections_box_backspace_key() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - app.data.radarr_data.collections.search = Some("Test".into()); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, - &mut app, - &ActiveRadarrBlock::SearchCollection, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .search - .as_ref() - .unwrap() - .text, - "Tes" - ); - } - - #[test] - fn test_filter_collections_box_backspace_key() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - app.data.radarr_data.collections.filter = Some("Test".into()); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, - &mut app, - &ActiveRadarrBlock::FilterCollections, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .filter - .as_ref() - .unwrap() - .text, - "Tes" - ); - } - - #[test] - fn test_search_collections_box_char_key() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - app.data.radarr_data.collections.search = Some(HorizontallyScrollableText::default()); - - CollectionsHandler::with( - &Key::Char('h'), - &mut app, - &ActiveRadarrBlock::SearchCollection, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .search - .as_ref() - .unwrap() - .text, - "h" - ); - } - - #[test] - fn test_filter_collections_box_char_key() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - app.data.radarr_data.collections.filter = Some(HorizontallyScrollableText::default()); - - CollectionsHandler::with( - &Key::Char('h'), - &mut app, - &ActiveRadarrBlock::FilterCollections, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .collections - .filter - .as_ref() - .unwrap() - .text, - "h" - ); - } - - #[test] - fn test_sort_key() { - let mut app = App::default(); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.sort.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::CollectionsSortPrompt.into() - ); - assert_eq!( - app - .data - .radarr_data - .collections - .sort - .as_ref() - .unwrap() - .items, - collections_sorting_options() - ); - assert!(!app.data.radarr_data.collections.sort_asc); - } - - #[test] - fn test_sort_key_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); - app - .data - .radarr_data - .collections - .set_items(vec![Collection::default()]); - - CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.sort.key, - &mut app, - &ActiveRadarrBlock::Collections, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Collections.into() - ); - assert!(app.data.radarr_data.collections.sort.is_none()); - assert!(!app.data.radarr_data.collections.sort_asc); - } - #[test] fn test_update_all_collections_prompt_confirm_confirm() { let mut app = App::default(); @@ -1523,10 +412,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into()); CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::UpdateAllCollectionsPrompt, - &None, + ActiveRadarrBlock::UpdateAllCollectionsPrompt, + None, ) .handle(); @@ -1537,7 +426,7 @@ mod tests { ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); } } @@ -1693,9 +582,9 @@ mod tests { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if collections_handler_blocks.contains(&active_radarr_block) { - assert!(CollectionsHandler::accepts(&active_radarr_block)); + assert!(CollectionsHandler::accepts(active_radarr_block)); } else { - assert!(!CollectionsHandler::accepts(&active_radarr_block)); + assert!(!CollectionsHandler::accepts(active_radarr_block)); } }); } @@ -1706,10 +595,10 @@ mod tests { app.is_loading = true; let handler = CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ); assert!(!handler.is_ready()); @@ -1721,10 +610,10 @@ mod tests { app.is_loading = false; let handler = CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ); assert!(!handler.is_ready()); @@ -1741,10 +630,10 @@ mod tests { .set_items(vec![Collection::default()]); let handler = CollectionsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Collections, - &None, + ActiveRadarrBlock::Collections, + None, ); assert!(handler.is_ready()); @@ -1784,16 +673,4 @@ mod tests { }, ] } - - fn sort_options() -> Vec> { - vec![SortOption { - name: "Test 1", - cmp_fn: Some(|a, b| { - b.title - .text - .to_lowercase() - .cmp(&a.title.text.to_lowercase()) - }), - }] - } } diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs index 5a7c1bc..f29f8fd 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler.rs @@ -12,22 +12,22 @@ use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; mod edit_collection_handler_tests; pub(super) struct EditCollectionHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + context: Option, } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - EDIT_COLLECTION_BLOCKS.contains(active_block) + fn accepts(active_block: ActiveRadarrBlock) -> bool { + EDIT_COLLECTION_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> EditCollectionHandler<'a, 'b> { EditCollectionHandler { key, @@ -37,7 +37,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -65,9 +65,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle .unwrap() .quality_profile_list .scroll_up(), - ActiveRadarrBlock::EditCollectionPrompt => { - self.app.data.radarr_data.selected_block.previous() - } + ActiveRadarrBlock::EditCollectionPrompt => self.app.data.radarr_data.selected_block.up(), _ => (), } } @@ -92,7 +90,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle .unwrap() .quality_profile_list .scroll_down(), - ActiveRadarrBlock::EditCollectionPrompt => self.app.data.radarr_data.selected_block.next(), + ActiveRadarrBlock::EditCollectionPrompt => self.app.data.radarr_data.selected_block.down(), _ => (), } } @@ -203,8 +201,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle | ActiveRadarrBlock::EditCollectionSelectQualityProfile => { self.app.push_navigation_stack( ( - *self.app.data.radarr_data.selected_block.get_active_block(), - *self.context, + self.app.data.radarr_data.selected_block.get_active_block(), + self.context, ) .into(), ) @@ -212,8 +210,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle ActiveRadarrBlock::EditCollectionRootFolderPathInput => { self.app.push_navigation_stack( ( - *self.app.data.radarr_data.selected_block.get_active_block(), - *self.context, + self.app.data.radarr_data.selected_block.get_active_block(), + self.context, ) .into(), ); @@ -308,8 +306,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditCollectionHandle } ActiveRadarrBlock::EditCollectionPrompt => { if self.app.data.radarr_data.selected_block.get_active_block() - == &ActiveRadarrBlock::EditCollectionConfirmPrompt - && *key == DEFAULT_KEYBINDINGS.confirm.key + == ActiveRadarrBlock::EditCollectionConfirmPrompt + && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection(None)); diff --git a/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs b/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs index 395b574..d8f9f5b 100644 --- a/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/edit_collection_handler_tests.rs @@ -44,10 +44,10 @@ mod tests { if key == Key::Up { for i in (0..minimum_availability_vec.len()).rev() { EditCollectionHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, - &None, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + None, ) .handle(); @@ -66,10 +66,10 @@ mod tests { } else { for i in 0..minimum_availability_vec.len() { EditCollectionHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, - &None, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + None, ) .handle(); @@ -104,10 +104,10 @@ mod tests { .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); EditCollectionHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::EditCollectionSelectQualityProfile, - &None, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + None, ) .handle(); @@ -124,10 +124,10 @@ mod tests { ); EditCollectionHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::EditCollectionSelectQualityProfile, - &None, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + None, ) .handle(); @@ -149,26 +149,21 @@ mod tests { let mut app = App::default(); app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - EditCollectionHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, - ) - .handle(); + EditCollectionHandler::with(key, &mut app, ActiveRadarrBlock::EditCollectionPrompt, None) + .handle(); if key == Key::Up { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditCollectionToggleMonitored + ActiveRadarrBlock::EditCollectionToggleMonitored ); } else { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditCollectionSelectQualityProfile + ActiveRadarrBlock::EditCollectionSelectQualityProfile ); } } @@ -181,20 +176,15 @@ mod tests { app.is_loading = true; app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - EditCollectionHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, - ) - .handle(); + EditCollectionHandler::with(key, &mut app, ActiveRadarrBlock::EditCollectionPrompt, None) + .handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditCollectionSelectMinimumAvailability + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability ); } } @@ -224,10 +214,10 @@ mod tests { .set_items(minimum_availability_vec.clone()); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, - &None, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + None, ) .handle(); @@ -244,10 +234,10 @@ mod tests { ); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, - &None, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + None, ) .handle(); @@ -282,10 +272,10 @@ mod tests { ]); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditCollectionSelectQualityProfile, - &None, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + None, ) .handle(); @@ -302,10 +292,10 @@ mod tests { ); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditCollectionSelectQualityProfile, - &None, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + None, ) .handle(); @@ -331,10 +321,10 @@ mod tests { }); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - &None, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + None, ) .handle(); @@ -352,10 +342,10 @@ mod tests { ); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - &None, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + None, ) .handle(); @@ -386,23 +376,13 @@ mod tests { fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { let mut app = App::default(); - EditCollectionHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, - ) - .handle(); + EditCollectionHandler::with(key, &mut app, ActiveRadarrBlock::EditCollectionPrompt, None) + .handle(); assert!(app.data.radarr_data.prompt_confirm); - EditCollectionHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, - ) - .handle(); + EditCollectionHandler::with(key, &mut app, ActiveRadarrBlock::EditCollectionPrompt, None) + .handle(); assert!(!app.data.radarr_data.prompt_confirm); } @@ -416,10 +396,10 @@ mod tests { }); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - &None, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + None, ) .handle(); @@ -437,10 +417,10 @@ mod tests { ); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - &None, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + None, ) .handle(); @@ -484,10 +464,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditCollectionRootFolderPathInput.into()); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - &None, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + None, ) .handle(); @@ -503,7 +483,7 @@ mod tests { .is_empty()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditCollectionPrompt.into() + ActiveRadarrBlock::EditCollectionPrompt.into() ); } @@ -514,24 +494,24 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, + ActiveRadarrBlock::EditCollectionPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); } @@ -544,24 +524,24 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); app.data.radarr_data.prompt_confirm = true; app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, + ActiveRadarrBlock::EditCollectionPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert_eq!( app.data.radarr_data.prompt_confirm_action, @@ -579,24 +559,24 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); app.data.radarr_data.prompt_confirm = true; app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, + ActiveRadarrBlock::EditCollectionPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditCollectionPrompt.into() + ActiveRadarrBlock::EditCollectionPrompt.into() ); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert!(!app.should_refresh); @@ -611,18 +591,18 @@ mod tests { let mut app = App::default(); app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); app.push_navigation_stack(current_route); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &Some(ActiveRadarrBlock::Collections), + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::Collections), ) .handle(); - assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.get_current_route(), current_route); assert_eq!( app .data @@ -635,14 +615,14 @@ mod tests { ); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &Some(ActiveRadarrBlock::Collections), + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::Collections), ) .handle(); - assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.get_current_route(), current_route); assert_eq!( app .data @@ -664,23 +644,23 @@ mod tests { let mut app = App::default(); app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 2); + .set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 2); app.push_navigation_stack(current_route); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &Some(ActiveRadarrBlock::Collections), + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::Collections), ) .handle(); - assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.get_current_route(), current_route); assert_eq!( app .data @@ -693,14 +673,14 @@ mod tests { ); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &Some(ActiveRadarrBlock::Collections), + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::Collections), ) .handle(); - assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.get_current_route(), current_route); assert_eq!( app .data @@ -731,20 +711,20 @@ mod tests { .into(), ); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(index); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(0, index); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &Some(ActiveRadarrBlock::Collections), + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::Collections), ) .handle(); assert_eq!( app.get_current_route(), - &(selected_block, Some(ActiveRadarrBlock::Collections)).into() + (selected_block, Some(ActiveRadarrBlock::Collections)).into() ); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); @@ -768,16 +748,16 @@ mod tests { app.push_navigation_stack(active_radarr_block.into()); EditCollectionHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &active_radarr_block, - &Some(ActiveRadarrBlock::Collections), + active_radarr_block, + Some(ActiveRadarrBlock::Collections), ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditCollectionPrompt.into() + ActiveRadarrBlock::EditCollectionPrompt.into() ); if active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput { @@ -806,17 +786,17 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditCollectionRootFolderPathInput.into()); EditCollectionHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - &None, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + None, ) .handle(); assert!(!app.should_ignore_quit_key); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditCollectionPrompt.into() + ActiveRadarrBlock::EditCollectionPrompt.into() ); } @@ -828,16 +808,16 @@ mod tests { app.data.radarr_data = create_test_radarr_data(); EditCollectionHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, + ActiveRadarrBlock::EditCollectionPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); let radarr_data = &app.data.radarr_data; @@ -860,11 +840,11 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(active_radarr_block.into()); - EditCollectionHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + EditCollectionHandler::with(ESC_KEY, &mut app, active_radarr_block, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); } } @@ -890,10 +870,10 @@ mod tests { }); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - &None, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + None, ) .handle(); @@ -916,10 +896,10 @@ mod tests { app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); EditCollectionHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - &None, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + None, ) .handle(); @@ -943,24 +923,24 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, + ActiveRadarrBlock::EditCollectionPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert_eq!( app.data.radarr_data.prompt_confirm_action, @@ -974,9 +954,9 @@ mod tests { fn test_edit_collection_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if EDIT_COLLECTION_BLOCKS.contains(&active_radarr_block) { - assert!(EditCollectionHandler::accepts(&active_radarr_block)); + assert!(EditCollectionHandler::accepts(active_radarr_block)); } else { - assert!(!EditCollectionHandler::accepts(&active_radarr_block)); + assert!(!EditCollectionHandler::accepts(active_radarr_block)); } }); } @@ -987,10 +967,10 @@ mod tests { app.is_loading = true; let handler = EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, + ActiveRadarrBlock::EditCollectionPrompt, + None, ); assert!(!handler.is_ready()); @@ -1002,10 +982,10 @@ mod tests { app.is_loading = false; let handler = EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, + ActiveRadarrBlock::EditCollectionPrompt, + None, ); assert!(!handler.is_ready()); @@ -1018,10 +998,10 @@ mod tests { app.data.radarr_data.edit_collection_modal = Some(EditCollectionModal::default()); let handler = EditCollectionHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::EditCollectionPrompt, - &None, + ActiveRadarrBlock::EditCollectionPrompt, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/collections/mod.rs b/src/handlers/radarr_handlers/collections/mod.rs index 595262f..265a03d 100644 --- a/src/handlers/radarr_handlers/collections/mod.rs +++ b/src/handlers/radarr_handlers/collections/mod.rs @@ -1,18 +1,19 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; +use crate::handle_table_events; use crate::handlers::radarr_handlers::collections::collection_details_handler::CollectionDetailsHandler; use crate::handlers::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::radarr_models::Collection; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, }; use crate::models::stateful_table::SortOption; -use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable}; +use crate::models::BlockSelectionState; use crate::network::radarr_network::RadarrEvent; -use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; mod collection_details_handler; mod edit_collection_handler; @@ -22,38 +23,66 @@ mod edit_collection_handler; mod collections_handler_tests; pub(super) struct CollectionsHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + context: Option, +} + +impl<'a, 'b> CollectionsHandler<'a, 'b> { + handle_table_events!( + self, + collections, + self.app.data.radarr_data.collections, + Collection + ); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'a, 'b> { fn handle(&mut self) { - match self.active_radarr_block { - _ if CollectionDetailsHandler::accepts(self.active_radarr_block) => { - CollectionDetailsHandler::with(self.key, self.app, self.active_radarr_block, self.context) + let collections_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::Collections.into()) + .sorting_block(ActiveRadarrBlock::CollectionsSortPrompt.into()) + .sort_by_fn(|a: &Collection, b: &Collection| a.id.cmp(&b.id)) + .sort_options(collections_sorting_options()) + .searching_block(ActiveRadarrBlock::SearchCollection.into()) + .search_error_block(ActiveRadarrBlock::SearchCollectionError.into()) + .search_field_fn(|collection| &collection.title.text) + .filtering_block(ActiveRadarrBlock::FilterCollections.into()) + .filter_error_block(ActiveRadarrBlock::FilterCollectionsError.into()) + .filter_field_fn(|collection| &collection.title.text); + + if !self.handle_collections_table_events(collections_table_handling_config) { + match self.active_radarr_block { + _ if CollectionDetailsHandler::accepts(self.active_radarr_block) => { + CollectionDetailsHandler::with( + self.key, + self.app, + self.active_radarr_block, + self.context, + ) .handle(); + } + _ if EditCollectionHandler::accepts(self.active_radarr_block) => { + EditCollectionHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle(); + } + _ => self.handle_key_event(), } - _ if EditCollectionHandler::accepts(self.active_radarr_block) => { - EditCollectionHandler::with(self.key, self.app, self.active_radarr_block, self.context) - .handle(); - } - _ => self.handle_key_event(), } } - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { + fn accepts(active_block: ActiveRadarrBlock) -> bool { CollectionDetailsHandler::accepts(active_block) || EditCollectionHandler::accepts(active_block) - || COLLECTIONS_BLOCKS.contains(active_block) + || COLLECTIONS_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> CollectionsHandler<'a, 'b> { CollectionsHandler { key, @@ -63,7 +92,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -71,105 +100,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' !self.app.is_loading && !self.app.data.radarr_data.collections.is_empty() } - fn handle_scroll_up(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_up(), - ActiveRadarrBlock::CollectionsSortPrompt => self - .app - .data - .radarr_data - .collections - .sort - .as_mut() - .unwrap() - .scroll_up(), - _ => (), - } - } + fn handle_scroll_up(&mut self) {} - fn handle_scroll_down(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_down(), - ActiveRadarrBlock::CollectionsSortPrompt => self - .app - .data - .radarr_data - .collections - .sort - .as_mut() - .unwrap() - .scroll_down(), - _ => (), - } - } + fn handle_scroll_down(&mut self) {} - fn handle_home(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_to_top(), - ActiveRadarrBlock::SearchCollection => self - .app - .data - .radarr_data - .collections - .search - .as_mut() - .unwrap() - .scroll_home(), - ActiveRadarrBlock::FilterCollections => self - .app - .data - .radarr_data - .collections - .filter - .as_mut() - .unwrap() - .scroll_home(), - ActiveRadarrBlock::CollectionsSortPrompt => self - .app - .data - .radarr_data - .collections - .sort - .as_mut() - .unwrap() - .scroll_to_top(), - _ => (), - } - } + fn handle_home(&mut self) {} - fn handle_end(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_to_bottom(), - ActiveRadarrBlock::SearchCollection => self - .app - .data - .radarr_data - .collections - .search - .as_mut() - .unwrap() - .reset_offset(), - ActiveRadarrBlock::FilterCollections => self - .app - .data - .radarr_data - .collections - .filter - .as_mut() - .unwrap() - .reset_offset(), - ActiveRadarrBlock::CollectionsSortPrompt => self - .app - .data - .radarr_data - .collections - .sort - .as_mut() - .unwrap() - .scroll_to_bottom(), - _ => (), - } - } + fn handle_end(&mut self) {} fn handle_delete(&mut self) {} @@ -177,34 +114,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' match self.active_radarr_block { ActiveRadarrBlock::Collections => handle_change_tab_left_right_keys(self.app, self.key), ActiveRadarrBlock::UpdateAllCollectionsPrompt => handle_prompt_toggle(self.app, self.key), - ActiveRadarrBlock::SearchCollection => { - handle_text_box_left_right_keys!( - self, - self.key, - self - .app - .data - .radarr_data - .collections - .search - .as_mut() - .unwrap() - ) - } - ActiveRadarrBlock::FilterCollections => { - handle_text_box_left_right_keys!( - self, - self.key, - self - .app - .data - .radarr_data - .collections - .filter - .as_mut() - .unwrap() - ) - } _ => (), } } @@ -214,44 +123,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' ActiveRadarrBlock::Collections => self .app .push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()), - ActiveRadarrBlock::SearchCollection => { - self.app.pop_navigation_stack(); - self.app.should_ignore_quit_key = false; - - if self.app.data.radarr_data.collections.search.is_some() { - let has_match = self - .app - .data - .radarr_data - .collections - .apply_search(|collection| &collection.title.text); - - if !has_match { - self - .app - .push_navigation_stack(ActiveRadarrBlock::SearchCollectionError.into()); - } - } - } - ActiveRadarrBlock::FilterCollections => { - self.app.pop_navigation_stack(); - self.app.should_ignore_quit_key = false; - - if self.app.data.radarr_data.collections.filter.is_some() { - let has_matches = self - .app - .data - .radarr_data - .collections - .apply_filter(|collection| &collection.title.text); - - if !has_matches { - self - .app - .push_navigation_stack(ActiveRadarrBlock::FilterCollectionsError.into()); - } - } - } ActiveRadarrBlock::UpdateAllCollectionsPrompt => { if self.app.data.radarr_data.prompt_confirm { self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections); @@ -259,44 +130,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' self.app.pop_navigation_stack(); } - ActiveRadarrBlock::CollectionsSortPrompt => { - self - .app - .data - .radarr_data - .collections - .items - .sort_by(|a, b| a.id.cmp(&b.id)); - self.app.data.radarr_data.collections.apply_sorting(); - - self.app.pop_navigation_stack(); - } _ => (), } } fn handle_esc(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::FilterCollections | ActiveRadarrBlock::FilterCollectionsError => { - self.app.pop_navigation_stack(); - self.app.data.radarr_data.collections.reset_filter(); - self.app.should_ignore_quit_key = false; - } - ActiveRadarrBlock::SearchCollection | ActiveRadarrBlock::SearchCollectionError => { - self.app.pop_navigation_stack(); - self.app.data.radarr_data.collections.reset_search(); - self.app.should_ignore_quit_key = false; - } ActiveRadarrBlock::UpdateAllCollectionsPrompt => { self.app.pop_navigation_stack(); self.app.data.radarr_data.prompt_confirm = false; } - ActiveRadarrBlock::CollectionsSortPrompt => { - self.app.pop_navigation_stack(); - } _ => { - self.app.data.radarr_data.collections.reset_search(); - self.app.data.radarr_data.collections.reset_filter(); handle_clear_errors(self.app); } } @@ -306,87 +150,27 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' let key = self.key; match self.active_radarr_block { ActiveRadarrBlock::Collections => match self.key { - _ if *key == DEFAULT_KEYBINDINGS.search.key => { + _ if key == DEFAULT_KEYBINDINGS.edit.key => { self .app - .push_navigation_stack(ActiveRadarrBlock::SearchCollection.into()); - self.app.data.radarr_data.collections.search = - Some(HorizontallyScrollableText::default()); - self.app.should_ignore_quit_key = true; - } - _ if *key == DEFAULT_KEYBINDINGS.filter.key => { - self - .app - .push_navigation_stack(ActiveRadarrBlock::FilterCollections.into()); - self.app.data.radarr_data.collections.reset_filter(); - self.app.data.radarr_data.collections.filter = - Some(HorizontallyScrollableText::default()); - self.app.should_ignore_quit_key = true; - } - _ if *key == DEFAULT_KEYBINDINGS.edit.key => { - self.app.push_navigation_stack( - ( - ActiveRadarrBlock::EditCollectionPrompt, - Some(ActiveRadarrBlock::Collections), - ) - .into(), - ); + .push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); self.app.data.radarr_data.edit_collection_modal = Some((&self.app.data.radarr_data).into()); self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_COLLECTION_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_COLLECTION_SELECTION_BLOCKS); } - _ if *key == DEFAULT_KEYBINDINGS.update.key => { + _ if key == DEFAULT_KEYBINDINGS.update.key => { self .app .push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into()); } - _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } - _ if *key == DEFAULT_KEYBINDINGS.sort.key => { - self - .app - .data - .radarr_data - .collections - .sorting(collections_sorting_options()); - self - .app - .push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into()); - } _ => (), }, - ActiveRadarrBlock::SearchCollection => { - handle_text_box_keys!( - self, - key, - self - .app - .data - .radarr_data - .collections - .search - .as_mut() - .unwrap() - ) - } - ActiveRadarrBlock::FilterCollections => { - handle_text_box_keys!( - self, - key, - self - .app - .data - .radarr_data - .collections - .filter - .as_mut() - .unwrap() - ) - } ActiveRadarrBlock::UpdateAllCollectionsPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { + if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateCollections); diff --git a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs index 17fed42..e5e75bc 100644 --- a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs +++ b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod tests { - use pretty_assertions::assert_str_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; @@ -11,109 +10,6 @@ mod tests { use crate::models::radarr_models::DownloadRecord; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; - mod test_handle_scroll_up_and_down { - use rstest::rstest; - - use crate::models::radarr_models::DownloadRecord; - use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; - - use super::*; - - test_iterable_scroll!( - test_downloads_scroll, - DownloadsHandler, - downloads, - DownloadRecord, - ActiveRadarrBlock::Downloads, - None, - title - ); - - #[rstest] - fn test_downloads_scroll_no_op_when_not_ready( - #[values( - DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key - )] - key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .downloads - .set_items(simple_stateful_iterable_vec!(DownloadRecord)); - - DownloadsHandler::with(&key, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); - - assert_str_eq!( - app.data.radarr_data.downloads.current_selection().title, - "Test 1" - ); - - DownloadsHandler::with(&key, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); - - assert_str_eq!( - app.data.radarr_data.downloads.current_selection().title, - "Test 1" - ); - } - } - - mod test_handle_home_end { - use crate::models::radarr_models::DownloadRecord; - use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; - - use super::*; - - test_iterable_home_and_end!( - test_downloads_home_end, - DownloadsHandler, - downloads, - DownloadRecord, - ActiveRadarrBlock::Downloads, - None, - title - ); - - #[test] - fn test_downloads_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .downloads - .set_items(extended_stateful_iterable_vec!(DownloadRecord)); - - DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Downloads, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.downloads.current_selection().title, - "Test 1" - ); - - DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Downloads, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.downloads.current_selection().title, - "Test 1" - ); - } - } - mod test_handle_delete { use pretty_assertions::assert_eq; @@ -130,11 +26,11 @@ mod tests { .downloads .set_items(vec![DownloadRecord::default()]); - DownloadsHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); + DownloadsHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::DeleteDownloadPrompt.into() + ActiveRadarrBlock::DeleteDownloadPrompt.into() ); } @@ -149,12 +45,9 @@ mod tests { .downloads .set_items(vec![DownloadRecord::default()]); - DownloadsHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); + DownloadsHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into()); } } @@ -171,20 +64,20 @@ mod tests { app.data.radarr_data.main_tabs.set_index(2); DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::Downloads, - &None, + ActiveRadarrBlock::Downloads, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); } @@ -195,21 +88,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(2); DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::Downloads, - &None, + ActiveRadarrBlock::Downloads, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Blocklist.into() - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() + ActiveRadarrBlock::Blocklist.into() ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); } #[rstest] @@ -223,11 +113,11 @@ mod tests { ) { let mut app = App::default(); - DownloadsHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); + DownloadsHandler::with(key, &mut app, active_radarr_block, None).handle(); assert!(app.data.radarr_data.prompt_confirm); - DownloadsHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); + DownloadsHandler::with(key, &mut app, active_radarr_block, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); } @@ -269,14 +159,14 @@ mod tests { app.push_navigation_stack(base_route.into()); app.push_navigation_stack(prompt_block.into()); - DownloadsHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); + DownloadsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(expected_action) ); - assert_eq!(app.get_current_route(), &base_route.into()); + assert_eq!(app.get_current_route(), base_route.into()); } #[rstest] @@ -295,11 +185,11 @@ mod tests { app.push_navigation_stack(base_route.into()); app.push_navigation_stack(prompt_block.into()); - DownloadsHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); + DownloadsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); - assert_eq!(app.get_current_route(), &base_route.into()); + assert_eq!(app.get_current_route(), base_route.into()); } } @@ -323,9 +213,9 @@ mod tests { app.push_navigation_stack(prompt_block.into()); app.data.radarr_data.prompt_confirm = true; - DownloadsHandler::with(&ESC_KEY, &mut app, &prompt_block, &None).handle(); + DownloadsHandler::with(ESC_KEY, &mut app, prompt_block, None).handle(); - assert_eq!(app.get_current_route(), &base_block.into()); + assert_eq!(app.get_current_route(), base_block.into()); assert!(!app.data.radarr_data.prompt_confirm); } @@ -337,12 +227,9 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); - DownloadsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Downloads, &None).handle(); + DownloadsHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::Downloads, None).handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into()); assert!(app.error.text.is_empty()); } } @@ -365,16 +252,16 @@ mod tests { .set_items(vec![DownloadRecord::default()]); DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::Downloads, - &None, + ActiveRadarrBlock::Downloads, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::UpdateDownloadsPrompt.into() + ActiveRadarrBlock::UpdateDownloadsPrompt.into() ); } @@ -390,17 +277,14 @@ mod tests { .set_items(vec![DownloadRecord::default()]); DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::Downloads, - &None, + ActiveRadarrBlock::Downloads, + None, ) .handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into()); } #[test] @@ -414,17 +298,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Downloads.into()); DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Downloads, - &None, + ActiveRadarrBlock::Downloads, + None, ) .handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into()); assert!(app.should_refresh); } @@ -440,17 +321,14 @@ mod tests { .set_items(vec![DownloadRecord::default()]); DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Downloads, - &None, + ActiveRadarrBlock::Downloads, + None, ) .handle(); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Downloads.into()); assert!(!app.should_refresh); } @@ -480,10 +358,10 @@ mod tests { app.push_navigation_stack(prompt_block.into()); DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &prompt_block, - &None, + prompt_block, + None, ) .handle(); @@ -492,7 +370,7 @@ mod tests { app.data.radarr_data.prompt_confirm_action, Some(expected_action) ); - assert_eq!(app.get_current_route(), &base_route.into()); + assert_eq!(app.get_current_route(), base_route.into()); } } @@ -500,9 +378,9 @@ mod tests { fn test_downloads_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if DOWNLOADS_BLOCKS.contains(&active_radarr_block) { - assert!(DownloadsHandler::accepts(&active_radarr_block)); + assert!(DownloadsHandler::accepts(active_radarr_block)); } else { - assert!(!DownloadsHandler::accepts(&active_radarr_block)); + assert!(!DownloadsHandler::accepts(active_radarr_block)); } }) } @@ -513,10 +391,10 @@ mod tests { app.is_loading = true; let handler = DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Downloads, - &None, + ActiveRadarrBlock::Downloads, + None, ); assert!(!handler.is_ready()); @@ -528,10 +406,10 @@ mod tests { app.is_loading = false; let handler = DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Downloads, - &None, + ActiveRadarrBlock::Downloads, + None, ); assert!(!handler.is_ready()); @@ -548,10 +426,10 @@ mod tests { .downloads .set_items(vec![DownloadRecord::default()]); let handler = DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Downloads, - &None, + ActiveRadarrBlock::Downloads, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/downloads/mod.rs b/src/handlers/radarr_handlers/downloads/mod.rs index d3b64a8..a09966f 100644 --- a/src/handlers/radarr_handlers/downloads/mod.rs +++ b/src/handlers/radarr_handlers/downloads/mod.rs @@ -1,10 +1,12 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; +use crate::handle_table_events; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::DownloadRecord; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLOADS_BLOCKS}; -use crate::models::Scrollable; use crate::network::radarr_network::RadarrEvent; #[cfg(test)] @@ -12,22 +14,40 @@ use crate::network::radarr_network::RadarrEvent; mod downloads_handler_tests; pub(super) struct DownloadsHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, +} + +impl<'a, 'b> DownloadsHandler<'a, 'b> { + handle_table_events!( + self, + downloads, + self.app.data.radarr_data.downloads, + DownloadRecord + ); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - DOWNLOADS_BLOCKS.contains(active_block) + fn handle(&mut self) { + let downloads_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::Downloads.into()); + + if !self.handle_downloads_table_events(downloads_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveRadarrBlock) -> bool { + DOWNLOADS_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_block: ActiveRadarrBlock, + _context: Option, ) -> DownloadsHandler<'a, 'b> { DownloadsHandler { key, @@ -37,7 +57,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -45,32 +65,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, !self.app.is_loading && !self.app.data.radarr_data.downloads.is_empty() } - fn handle_scroll_up(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Downloads { - self.app.data.radarr_data.downloads.scroll_up() - } - } + fn handle_scroll_up(&mut self) {} - fn handle_scroll_down(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Downloads { - self.app.data.radarr_data.downloads.scroll_down() - } - } + fn handle_scroll_down(&mut self) {} - fn handle_home(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Downloads { - self.app.data.radarr_data.downloads.scroll_to_top() - } - } + fn handle_home(&mut self) {} - fn handle_end(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Downloads { - self.app.data.radarr_data.downloads.scroll_to_bottom() - } - } + fn handle_end(&mut self) {} fn handle_delete(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Downloads { + if self.active_radarr_block == ActiveRadarrBlock::Downloads { self .app .push_navigation_stack(ActiveRadarrBlock::DeleteDownloadPrompt.into()) @@ -121,18 +125,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, let key = self.key; match self.active_radarr_block { ActiveRadarrBlock::Downloads => match self.key { - _ if *key == DEFAULT_KEYBINDINGS.update.key => { + _ if key == DEFAULT_KEYBINDINGS.update.key => { self .app .push_navigation_stack(ActiveRadarrBlock::UpdateDownloadsPrompt.into()); } - _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } _ => (), }, ActiveRadarrBlock::DeleteDownloadPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { + if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteDownload(None)); @@ -140,7 +144,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DownloadsHandler<'a, } } ActiveRadarrBlock::UpdateDownloadsPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { + if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateDownloads); diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs index 3ed6856..7c0f936 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs @@ -4,29 +4,29 @@ use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; use crate::network::radarr_network::RadarrEvent; -use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; +use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys}; #[cfg(test)] #[path = "edit_indexer_handler_tests.rs"] mod edit_indexer_handler_tests; pub(super) struct EditIndexerHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - EDIT_INDEXER_BLOCKS.contains(active_block) + fn accepts(active_block: ActiveRadarrBlock) -> bool { + EDIT_INDEXER_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_block: ActiveRadarrBlock, + _context: Option, ) -> EditIndexerHandler<'a, 'b> { EditIndexerHandler { key, @@ -36,7 +36,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -45,14 +45,42 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' } fn handle_scroll_up(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::EditIndexerPrompt { - self.app.data.radarr_data.selected_block.previous(); + match self.active_radarr_block { + ActiveRadarrBlock::EditIndexerPrompt => { + self.app.data.radarr_data.selected_block.up(); + } + ActiveRadarrBlock::EditIndexerPriorityInput => { + self + .app + .data + .radarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .priority += 1; + } + _ => (), } } fn handle_scroll_down(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::EditIndexerPrompt { - self.app.data.radarr_data.selected_block.next(); + match self.active_radarr_block { + ActiveRadarrBlock::EditIndexerPrompt => { + self.app.data.radarr_data.selected_block.down(); + } + ActiveRadarrBlock::EditIndexerPriorityInput => { + let edit_indexer_modal = self + .app + .data + .radarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + if edit_indexer_modal.priority > 0 { + edit_indexer_modal.priority -= 1; + } + } + _ => (), } } @@ -183,15 +211,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' fn handle_left_right_action(&mut self) { match self.active_radarr_block { ActiveRadarrBlock::EditIndexerPrompt => { - if self.app.data.radarr_data.selected_block.get_active_block() - == &ActiveRadarrBlock::EditIndexerConfirmPrompt - { - handle_prompt_toggle(self.app, self.key); - } else { - let len = self.app.data.radarr_data.selected_block.blocks.len(); - let idx = self.app.data.radarr_data.selected_block.index; - self.app.data.radarr_data.selected_block.index = (idx + 5) % len; - } + handle_prompt_left_right_keys!( + self, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + radarr_data + ); } ActiveRadarrBlock::EditIndexerNameInput => { handle_text_box_left_right_keys!( @@ -270,7 +294,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' fn handle_submit(&mut self) { match self.active_radarr_block { ActiveRadarrBlock::EditIndexerPrompt => { - let selected_block = *self.app.data.radarr_data.selected_block.get_active_block(); + let selected_block = self.app.data.radarr_data.selected_block.get_active_block(); match selected_block { ActiveRadarrBlock::EditIndexerConfirmPrompt => { let radarr_data = &mut self.app.data.radarr_data; @@ -291,6 +315,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' self.app.push_navigation_stack(selected_block.into()); self.app.should_ignore_quit_key = true; } + ActiveRadarrBlock::EditIndexerPriorityInput => self + .app + .push_navigation_stack(ActiveRadarrBlock::EditIndexerPriorityInput.into()), ActiveRadarrBlock::EditIndexerToggleEnableRss => { let indexer = self .app @@ -334,6 +361,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' self.app.pop_navigation_stack(); self.app.should_ignore_quit_key = false; } + ActiveRadarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(), _ => (), } } @@ -349,6 +377,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' | ActiveRadarrBlock::EditIndexerUrlInput | ActiveRadarrBlock::EditIndexerApiKeyInput | ActiveRadarrBlock::EditIndexerSeedRatioInput + | ActiveRadarrBlock::EditIndexerPriorityInput | ActiveRadarrBlock::EditIndexerTagsInput => { self.app.pop_navigation_stack(); self.app.should_ignore_quit_key = false; @@ -431,8 +460,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<' } ActiveRadarrBlock::EditIndexerPrompt => { if self.app.data.radarr_data.selected_block.get_active_block() - == &ActiveRadarrBlock::EditIndexerConfirmPrompt - && *self.key == DEFAULT_KEYBINDINGS.confirm.key + == ActiveRadarrBlock::EditIndexerConfirmPrompt + && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer(None)); diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs index 8c33809..9325c65 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -20,26 +20,102 @@ mod tests { use super::*; + #[rstest] + fn test_edit_indexer_priority_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + key, + &mut app, + ActiveRadarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 1 + ); + } else { + assert_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 0 + ); + + EditIndexerHandler::with( + Key::Up, + &mut app, + ActiveRadarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 1 + ); + + EditIndexerHandler::with( + key, + &mut app, + ActiveRadarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + assert_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 0 + ); + } + } + #[rstest] fn test_edit_indexer_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); if key == Key::Up { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditIndexerNameInput + ActiveRadarrBlock::EditIndexerNameInput ); } else { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch ); } } @@ -49,18 +125,18 @@ mod tests { #[values(Key::Up, Key::Down)] key: Key, ) { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.is_loading = true; app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditIndexerToggleEnableRss + ActiveRadarrBlock::EditIndexerToggleEnableRss ); } } @@ -77,16 +153,17 @@ mod tests { #[test] fn test_edit_indexer_name_input_home_end() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { name: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditIndexerNameInput, - &None, + ActiveRadarrBlock::EditIndexerNameInput, + None, ) .handle(); @@ -104,10 +181,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditIndexerNameInput, - &None, + ActiveRadarrBlock::EditIndexerNameInput, + None, ) .handle(); @@ -128,16 +205,17 @@ mod tests { #[test] fn test_edit_indexer_url_input_home_end() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { url: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditIndexerUrlInput, - &None, + ActiveRadarrBlock::EditIndexerUrlInput, + None, ) .handle(); @@ -155,10 +233,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditIndexerUrlInput, - &None, + ActiveRadarrBlock::EditIndexerUrlInput, + None, ) .handle(); @@ -179,16 +257,17 @@ mod tests { #[test] fn test_edit_indexer_api_key_input_home_end() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { api_key: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditIndexerApiKeyInput, - &None, + ActiveRadarrBlock::EditIndexerApiKeyInput, + None, ) .handle(); @@ -206,10 +285,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditIndexerApiKeyInput, - &None, + ActiveRadarrBlock::EditIndexerApiKeyInput, + None, ) .handle(); @@ -230,16 +309,17 @@ mod tests { #[test] fn test_edit_indexer_seed_ratio_input_home_end() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { seed_ratio: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditIndexerSeedRatioInput, - &None, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + None, ) .handle(); @@ -257,10 +337,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditIndexerSeedRatioInput, - &None, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + None, ) .handle(); @@ -281,16 +361,17 @@ mod tests { #[test] fn test_edit_indexer_tags_input_home_end() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { tags: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditIndexerTagsInput, - &None, + ActiveRadarrBlock::EditIndexerTagsInput, + None, ) .handle(); @@ -308,10 +389,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditIndexerTagsInput, - &None, + ActiveRadarrBlock::EditIndexerTagsInput, + None, ) .handle(); @@ -347,17 +428,16 @@ mod tests { #[rstest] fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.index = EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1; + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.y = EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1; - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); assert!(app.data.radarr_data.prompt_confirm); - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); } @@ -385,34 +465,33 @@ mod tests { )] fn test_left_right_block_toggle_torrents( #[values(Key::Left, Key::Right)] key: Key, - #[case] starting_index: usize, + #[case] starting_y_index: usize, #[case] left_block: ActiveRadarrBlock, #[case] right_block: ActiveRadarrBlock, ) { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.index = starting_index; + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.y = starting_y_index; assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &left_block + left_block ); - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &right_block + right_block ); - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &left_block + left_block ); } @@ -432,68 +511,71 @@ mod tests { ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, ActiveRadarrBlock::EditIndexerTagsInput )] + #[case( + 3, + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerPriorityInput + )] fn test_left_right_block_toggle_nzb( #[values(Key::Left, Key::Right)] key: Key, - #[case] starting_index: usize, + #[case] starting_y_index: usize, #[case] left_block: ActiveRadarrBlock, #[case] right_block: ActiveRadarrBlock, ) { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_NZB_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.index = starting_index; + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.y = starting_y_index; assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &left_block + left_block ); - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &right_block + right_block ); - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &left_block + left_block ); } #[rstest] - fn test_left_right_block_toggle_nzb_empty_row_to_prompt_confirm( + fn test_left_right_block_toggle_torren_empty_row_to_prompt_confirm( #[values(Key::Left, Key::Right)] key: Key, ) { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_NZB_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.index = 3; + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.y = 4; app.data.radarr_data.prompt_confirm = false; assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch + ActiveRadarrBlock::EditIndexerPriorityInput ); - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditIndexerConfirmPrompt + ActiveRadarrBlock::EditIndexerConfirmPrompt ); - EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) - .handle(); + EditIndexerHandler::with(key, &mut app, ActiveRadarrBlock::EditIndexerPrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditIndexerConfirmPrompt + ActiveRadarrBlock::EditIndexerConfirmPrompt ); assert!(app.data.radarr_data.prompt_confirm); } @@ -501,16 +583,17 @@ mod tests { #[test] fn test_edit_indexer_name_input_left_right_keys() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { name: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::EditIndexerNameInput, - &None, + ActiveRadarrBlock::EditIndexerNameInput, + None, ) .handle(); @@ -528,10 +611,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::EditIndexerNameInput, - &None, + ActiveRadarrBlock::EditIndexerNameInput, + None, ) .handle(); @@ -552,16 +635,17 @@ mod tests { #[test] fn test_edit_indexer_url_input_left_right_keys() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { url: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::EditIndexerUrlInput, - &None, + ActiveRadarrBlock::EditIndexerUrlInput, + None, ) .handle(); @@ -579,10 +663,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::EditIndexerUrlInput, - &None, + ActiveRadarrBlock::EditIndexerUrlInput, + None, ) .handle(); @@ -603,16 +687,17 @@ mod tests { #[test] fn test_edit_indexer_api_key_input_left_right_keys() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { api_key: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::EditIndexerApiKeyInput, - &None, + ActiveRadarrBlock::EditIndexerApiKeyInput, + None, ) .handle(); @@ -630,10 +715,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::EditIndexerApiKeyInput, - &None, + ActiveRadarrBlock::EditIndexerApiKeyInput, + None, ) .handle(); @@ -654,16 +739,17 @@ mod tests { #[test] fn test_edit_indexer_seed_ratio_input_left_right_keys() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { seed_ratio: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::EditIndexerSeedRatioInput, - &None, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + None, ) .handle(); @@ -681,10 +767,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::EditIndexerSeedRatioInput, - &None, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + None, ) .handle(); @@ -705,16 +791,17 @@ mod tests { #[test] fn test_edit_indexer_tags_input_left_right_keys() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { tags: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::EditIndexerTagsInput, - &None, + ActiveRadarrBlock::EditIndexerTagsInput, + None, ) .handle(); @@ -732,10 +819,10 @@ mod tests { ); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::EditIndexerTagsInput, - &None, + ActiveRadarrBlock::EditIndexerTagsInput, + None, ) .handle(); @@ -775,23 +862,23 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert!(!app.should_refresh); assert_eq!(app.data.radarr_data.edit_indexer_modal, None); @@ -803,24 +890,24 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.prompt_confirm = true; EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(app.data.radarr_data.edit_indexer_modal.is_some()); assert!(app.should_refresh); assert_eq!( @@ -839,16 +926,16 @@ mod tests { app.data.radarr_data.prompt_confirm = true; EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); assert!(app.data.radarr_data.edit_indexer_modal.is_some()); assert!(!app.should_refresh); @@ -856,58 +943,86 @@ mod tests { } #[rstest] - #[case(0, ActiveRadarrBlock::EditIndexerNameInput)] - #[case(5, ActiveRadarrBlock::EditIndexerUrlInput)] - #[case(6, ActiveRadarrBlock::EditIndexerApiKeyInput)] - #[case(7, ActiveRadarrBlock::EditIndexerSeedRatioInput)] - #[case(8, ActiveRadarrBlock::EditIndexerTagsInput)] + #[case(0, 0, ActiveRadarrBlock::EditIndexerNameInput)] + #[case(0, 1, ActiveRadarrBlock::EditIndexerUrlInput)] + #[case(1, 1, ActiveRadarrBlock::EditIndexerApiKeyInput)] + #[case(2, 1, ActiveRadarrBlock::EditIndexerSeedRatioInput)] + #[case(3, 1, ActiveRadarrBlock::EditIndexerTagsInput)] fn test_edit_indexer_prompt_submit_input_fields( - #[case] starting_index: usize, + #[case] starting_y: usize, + #[case] starting_x: usize, #[case] block: ActiveRadarrBlock, ) { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(starting_index); + .set_index(starting_x, starting_y); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &block.into()); + assert_eq!(app.get_current_route(), block.into()); assert!(app.should_ignore_quit_key); } #[test] - fn test_edit_indexer_toggle_enable_rss_submit() { + fn test_edit_indexer_priority_input_submit() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); - app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(1); app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.data.radarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(0, 4); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPriorityInput.into() + ); + assert!(!app.should_ignore_quit_key); + } + + #[test] + fn test_edit_indexer_toggle_enable_rss_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.radarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(0, 1); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveRadarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveRadarrBlock::EditIndexerPrompt.into() ); assert!(app .data @@ -919,16 +1034,16 @@ mod tests { .unwrap()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); assert!(!app .data @@ -943,23 +1058,24 @@ mod tests { #[test] fn test_edit_indexer_toggle_enable_automatic_search_submit() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(2); + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(0, 2); app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); assert!(app .data @@ -971,16 +1087,16 @@ mod tests { .unwrap()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); assert!(!app .data @@ -995,23 +1111,24 @@ mod tests { #[test] fn test_edit_indexer_toggle_enable_interactive_search_submit() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(3); + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(0, 3); app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); assert!(app .data @@ -1023,16 +1140,16 @@ mod tests { .unwrap()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); assert!(!app .data @@ -1047,6 +1164,7 @@ mod tests { #[test] fn test_edit_indexer_name_input_submit() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.should_ignore_quit_key = true; app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { name: "Test".into(), @@ -1056,10 +1174,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditIndexerNameInput.into()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerNameInput, - &None, + ActiveRadarrBlock::EditIndexerNameInput, + None, ) .handle(); @@ -1075,13 +1193,14 @@ mod tests { .is_empty()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); } #[test] fn test_edit_indexer_url_input_submit() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.should_ignore_quit_key = true; app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { url: "Test".into(), @@ -1091,10 +1210,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditIndexerUrlInput.into()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerUrlInput, - &None, + ActiveRadarrBlock::EditIndexerUrlInput, + None, ) .handle(); @@ -1110,13 +1229,14 @@ mod tests { .is_empty()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); } #[test] fn test_edit_indexer_api_key_input_submit() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.should_ignore_quit_key = true; app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { api_key: "Test".into(), @@ -1126,10 +1246,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditIndexerApiKeyInput.into()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerApiKeyInput, - &None, + ActiveRadarrBlock::EditIndexerApiKeyInput, + None, ) .handle(); @@ -1145,13 +1265,14 @@ mod tests { .is_empty()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); } #[test] fn test_edit_indexer_seed_ratio_input_submit() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.should_ignore_quit_key = true; app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { seed_ratio: "Test".into(), @@ -1161,10 +1282,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditIndexerSeedRatioInput.into()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerSeedRatioInput, - &None, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + None, ) .handle(); @@ -1180,13 +1301,14 @@ mod tests { .is_empty()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); } #[test] fn test_edit_indexer_tags_input_submit() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.should_ignore_quit_key = true; app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { tags: "Test".into(), @@ -1196,10 +1318,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditIndexerTagsInput.into()); EditIndexerHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerTagsInput, - &None, + ActiveRadarrBlock::EditIndexerTagsInput, + None, ) .handle(); @@ -1215,7 +1337,7 @@ mod tests { .is_empty()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); } } @@ -1239,14 +1361,14 @@ mod tests { app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); EditIndexerHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.data.radarr_data.edit_indexer_modal, None); } @@ -1258,7 +1380,8 @@ mod tests { ActiveRadarrBlock::EditIndexerUrlInput, ActiveRadarrBlock::EditIndexerApiKeyInput, ActiveRadarrBlock::EditIndexerSeedRatioInput, - ActiveRadarrBlock::EditIndexerTagsInput + ActiveRadarrBlock::EditIndexerTagsInput, + ActiveRadarrBlock::EditIndexerPriorityInput )] active_radarr_block: ActiveRadarrBlock, ) { @@ -1268,9 +1391,9 @@ mod tests { app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); app.should_ignore_quit_key = true; - EditIndexerHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + EditIndexerHandler::with(ESC_KEY, &mut app, active_radarr_block, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(!app.should_ignore_quit_key); assert_eq!( app.data.radarr_data.edit_indexer_modal, @@ -1292,16 +1415,17 @@ mod tests { #[test] fn test_edit_indexer_name_input_backspace() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { name: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::EditIndexerNameInput, - &None, + ActiveRadarrBlock::EditIndexerNameInput, + None, ) .handle(); @@ -1321,16 +1445,17 @@ mod tests { #[test] fn test_edit_indexer_url_input_backspace() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { url: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::EditIndexerUrlInput, - &None, + ActiveRadarrBlock::EditIndexerUrlInput, + None, ) .handle(); @@ -1350,16 +1475,17 @@ mod tests { #[test] fn test_edit_indexer_api_key_input_backspace() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { api_key: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::EditIndexerApiKeyInput, - &None, + ActiveRadarrBlock::EditIndexerApiKeyInput, + None, ) .handle(); @@ -1379,16 +1505,17 @@ mod tests { #[test] fn test_edit_indexer_seed_ratio_input_backspace() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { seed_ratio: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::EditIndexerSeedRatioInput, - &None, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + None, ) .handle(); @@ -1408,16 +1535,17 @@ mod tests { #[test] fn test_edit_indexer_tags_input_backspace() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { tags: "Test".into(), ..EditIndexerModal::default() }); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::EditIndexerTagsInput, - &None, + ActiveRadarrBlock::EditIndexerTagsInput, + None, ) .handle(); @@ -1437,13 +1565,14 @@ mod tests { #[test] fn test_edit_indexer_name_input_char_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); EditIndexerHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::EditIndexerNameInput, - &None, + ActiveRadarrBlock::EditIndexerNameInput, + None, ) .handle(); @@ -1463,13 +1592,14 @@ mod tests { #[test] fn test_edit_indexer_url_input_char_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); EditIndexerHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::EditIndexerUrlInput, - &None, + ActiveRadarrBlock::EditIndexerUrlInput, + None, ) .handle(); @@ -1489,13 +1619,14 @@ mod tests { #[test] fn test_edit_indexer_api_key_input_char_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); EditIndexerHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::EditIndexerApiKeyInput, - &None, + ActiveRadarrBlock::EditIndexerApiKeyInput, + None, ) .handle(); @@ -1515,13 +1646,14 @@ mod tests { #[test] fn test_edit_indexer_seed_ratio_input_char_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); EditIndexerHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::EditIndexerSeedRatioInput, - &None, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + None, ) .handle(); @@ -1541,13 +1673,14 @@ mod tests { #[test] fn test_edit_indexer_tags_input_char_key() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); EditIndexerHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::EditIndexerTagsInput, - &None, + ActiveRadarrBlock::EditIndexerTagsInput, + None, ) .handle(); @@ -1570,23 +1703,23 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(app.data.radarr_data.edit_indexer_modal.is_some()); assert!(app.should_refresh); assert_eq!( @@ -1597,12 +1730,12 @@ mod tests { } #[test] - fn test_indexer_settings_handler_accepts() { + fn test_edit_indexer_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if EDIT_INDEXER_BLOCKS.contains(&active_radarr_block) { - assert!(EditIndexerHandler::accepts(&active_radarr_block)); + assert!(EditIndexerHandler::accepts(active_radarr_block)); } else { - assert!(!EditIndexerHandler::accepts(&active_radarr_block)); + assert!(!EditIndexerHandler::accepts(active_radarr_block)); } }) } @@ -1610,13 +1743,14 @@ mod tests { #[test] fn test_edit_indexer_handler_is_not_ready_when_loading() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.is_loading = true; let handler = EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ); assert!(!handler.is_ready()); @@ -1625,13 +1759,14 @@ mod tests { #[test] fn test_edit_indexer_handler_is_not_ready_when_edit_indexer_modal_is_none() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.is_loading = false; let handler = EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ); assert!(!handler.is_ready()); @@ -1640,14 +1775,15 @@ mod tests { #[test] fn test_edit_indexer_handler_is_ready_when_edit_indexer_modal_is_some() { let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.is_loading = false; app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); let handler = EditIndexerHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::EditIndexerPrompt, - &None, + ActiveRadarrBlock::EditIndexerPrompt, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs index 0d3641c..046e7b5 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -6,29 +6,29 @@ use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS, }; use crate::network::radarr_network::RadarrEvent; -use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; +use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys}; #[cfg(test)] #[path = "edit_indexer_settings_handler_tests.rs"] mod edit_indexer_settings_handler_tests; pub(super) struct IndexerSettingsHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - INDEXER_SETTINGS_BLOCKS.contains(active_block) + fn accepts(active_block: ActiveRadarrBlock) -> bool { + INDEXER_SETTINGS_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_block: ActiveRadarrBlock, + _context: Option, ) -> IndexerSettingsHandler<'a, 'b> { IndexerSettingsHandler { key, @@ -38,7 +38,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -50,7 +50,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap(); match self.active_radarr_block { ActiveRadarrBlock::AllIndexerSettingsPrompt => { - self.app.data.radarr_data.selected_block.previous(); + self.app.data.radarr_data.selected_block.up(); } ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => { indexer_settings.minimum_age += 1; @@ -75,7 +75,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap(); match self.active_radarr_block { ActiveRadarrBlock::AllIndexerSettingsPrompt => { - self.app.data.radarr_data.selected_block.next() + self.app.data.radarr_data.selected_block.down() } ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => { if indexer_settings.minimum_age > 0 { @@ -105,7 +105,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl } fn handle_home(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput { + if self.active_radarr_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput { self .app .data @@ -119,7 +119,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl } fn handle_end(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput { + if self.active_radarr_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput { self .app .data @@ -137,15 +137,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl fn handle_left_right_action(&mut self) { match self.active_radarr_block { ActiveRadarrBlock::AllIndexerSettingsPrompt => { - if self.app.data.radarr_data.selected_block.get_active_block() - == &ActiveRadarrBlock::IndexerSettingsConfirmPrompt - { - handle_prompt_toggle(self.app, self.key); - } else { - let len = self.app.data.radarr_data.selected_block.blocks.len(); - let idx = self.app.data.radarr_data.selected_block.index; - self.app.data.radarr_data.selected_block.index = (idx + 5) % len; - } + handle_prompt_left_right_keys!( + self, + ActiveRadarrBlock::IndexerSettingsConfirmPrompt, + radarr_data + ); } ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput => { handle_text_box_left_right_keys!( @@ -187,7 +183,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl | ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput => { self.app.push_navigation_stack( ( - *self.app.data.radarr_data.selected_block.get_active_block(), + self.app.data.radarr_data.selected_block.get_active_block(), None, ) .into(), @@ -258,8 +254,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl } ActiveRadarrBlock::AllIndexerSettingsPrompt => { if self.app.data.radarr_data.selected_block.get_active_block() - == &ActiveRadarrBlock::IndexerSettingsConfirmPrompt - && *self.key == DEFAULT_KEYBINDINGS.confirm.key + == ActiveRadarrBlock::IndexerSettingsConfirmPrompt + && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs index 9371ed5..8d3bddf 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -27,7 +27,7 @@ mod tests { let mut app = App::default(); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); - IndexerSettingsHandler::with(&$key, &mut app, &$block, &None).handle(); + IndexerSettingsHandler::with($key, &mut app, $block, None).handle(); if $key == Key::Up { assert_eq!( @@ -64,7 +64,7 @@ mod tests { 0 ); - IndexerSettingsHandler::with(&Key::Up, &mut app, &$block, &None).handle(); + IndexerSettingsHandler::with(Key::Up, &mut app, $block, None).handle(); assert_eq!( app @@ -77,7 +77,7 @@ mod tests { 1 ); - IndexerSettingsHandler::with(&$key, &mut app, &$block, &None).handle(); + IndexerSettingsHandler::with($key, &mut app, $block, None).handle(); assert_eq!( app .data @@ -98,26 +98,26 @@ mod tests { let mut app = App::default(); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); IndexerSettingsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); if key == Key::Up { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput + ActiveRadarrBlock::IndexerSettingsMinimumAgeInput ); } else { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput + ActiveRadarrBlock::IndexerSettingsMaximumSizeInput ); } } @@ -130,20 +130,20 @@ mod tests { app.is_loading = true; app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); IndexerSettingsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::IndexerSettingsRetentionInput + ActiveRadarrBlock::IndexerSettingsRetentionInput ); } @@ -218,10 +218,10 @@ mod tests { }); IndexerSettingsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - &None, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + None, ) .handle(); @@ -239,10 +239,10 @@ mod tests { ); IndexerSettingsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - &None, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + None, ) .handle(); @@ -276,24 +276,24 @@ mod tests { fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { let mut app = App::default(); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.index = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1; + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.y = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1; IndexerSettingsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert!(app.data.radarr_data.prompt_confirm); IndexerSettingsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); @@ -323,44 +323,44 @@ mod tests { )] fn test_left_right_block_toggle( #[values(Key::Left, Key::Right)] key: Key, - #[case] starting_index: usize, + #[case] starting_y_index: usize, #[case] left_block: ActiveRadarrBlock, #[case] right_block: ActiveRadarrBlock, ) { let mut app = App::default(); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.index = starting_index; + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.y = starting_y_index; assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &left_block + left_block ); IndexerSettingsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &right_block + right_block ); IndexerSettingsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &left_block + left_block ); } @@ -373,10 +373,10 @@ mod tests { }); IndexerSettingsHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - &None, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + None, ) .handle(); @@ -394,10 +394,10 @@ mod tests { ); IndexerSettingsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - &None, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + None, ) .handle(); @@ -438,23 +438,23 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert!(!app.should_refresh); assert_eq!(app.data.radarr_data.indexer_settings, None); @@ -466,24 +466,24 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.data.radarr_data.prompt_confirm = true; IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::EditAllIndexerSettings(None)) @@ -502,71 +502,80 @@ mod tests { app.data.radarr_data.prompt_confirm = true; IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AllIndexerSettingsPrompt.into() + ActiveRadarrBlock::AllIndexerSettingsPrompt.into() ); assert!(!app.should_refresh); } #[rstest] - #[case(ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, 0)] - #[case(ActiveRadarrBlock::IndexerSettingsRetentionInput, 1)] - #[case(ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, 2)] - #[case(ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, 5)] - #[case(ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, 6)] + #[case(ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, 0, 0)] + #[case(ActiveRadarrBlock::IndexerSettingsRetentionInput, 1, 0)] + #[case(ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, 2, 0)] + #[case(ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, 0, 1)] + #[case(ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, 1, 1)] fn test_edit_indexer_settings_prompt_submit_selected_block( #[case] selected_block: ActiveRadarrBlock, - #[case] index: usize, + #[case] y_index: usize, + #[case] x_index: usize, ) { let mut app = App::default(); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(index); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .radarr_data + .selected_block + .set_index(x_index, y_index); IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &selected_block.into()); + assert_eq!(app.get_current_route(), selected_block.into()); } #[rstest] fn test_edit_indexer_settings_prompt_submit_selected_block_no_op_when_not_ready( - #[values(0, 1, 2, 5, 6)] index: usize, + #[values((0, 0), (1, 0), (2, 0), (0, 1), (1, 1))] index: (usize, usize), ) { let mut app = App::default(); app.is_loading = true; app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(index); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .radarr_data + .selected_block + .set_index(index.1, index.0); IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AllIndexerSettingsPrompt.into() + ActiveRadarrBlock::AllIndexerSettingsPrompt.into() ); } @@ -576,20 +585,20 @@ mod tests { app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(7); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(1, 2); IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into() + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput.into() ); assert!(app.should_ignore_quit_key); } @@ -599,21 +608,21 @@ mod tests { let mut app = App::default(); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(3); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(0, 3); app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AllIndexerSettingsPrompt.into() + ActiveRadarrBlock::AllIndexerSettingsPrompt.into() ); assert!( app @@ -626,16 +635,16 @@ mod tests { ); IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AllIndexerSettingsPrompt.into() + ActiveRadarrBlock::AllIndexerSettingsPrompt.into() ); assert!( !app @@ -653,21 +662,21 @@ mod tests { let mut app = App::default(); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(8); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(1, 3); app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AllIndexerSettingsPrompt.into() + ActiveRadarrBlock::AllIndexerSettingsPrompt.into() ); assert!( app @@ -680,16 +689,16 @@ mod tests { ); IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AllIndexerSettingsPrompt.into() + ActiveRadarrBlock::AllIndexerSettingsPrompt.into() ); assert!( !app @@ -716,10 +725,10 @@ mod tests { ); IndexerSettingsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - &None, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + None, ) .handle(); @@ -735,7 +744,7 @@ mod tests { .is_empty()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AllIndexerSettingsPrompt.into() + ActiveRadarrBlock::AllIndexerSettingsPrompt.into() ); } @@ -755,11 +764,11 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); app.push_navigation_stack(active_radarr_block.into()); - IndexerSettingsHandler::with(&SUBMIT_KEY, &mut app, &active_radarr_block, &None).handle(); + IndexerSettingsHandler::with(SUBMIT_KEY, &mut app, active_radarr_block, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AllIndexerSettingsPrompt.into() + ActiveRadarrBlock::AllIndexerSettingsPrompt.into() ); } } @@ -783,14 +792,14 @@ mod tests { app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); IndexerSettingsHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.data.radarr_data.indexer_settings, None); } @@ -806,14 +815,14 @@ mod tests { app.should_ignore_quit_key = true; IndexerSettingsHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - &None, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(!app.should_ignore_quit_key); assert_eq!( app.data.radarr_data.indexer_settings, @@ -838,9 +847,9 @@ mod tests { app.push_navigation_stack(active_radarr_block.into()); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); - IndexerSettingsHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + IndexerSettingsHandler::with(ESC_KEY, &mut app, active_radarr_block, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!( app.data.radarr_data.indexer_settings, Some(IndexerSettings::default()) @@ -870,10 +879,10 @@ mod tests { }); IndexerSettingsHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - &None, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + None, ) .handle(); @@ -896,10 +905,10 @@ mod tests { app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); IndexerSettingsHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - &None, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + None, ) .handle(); @@ -922,23 +931,23 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); IndexerSettingsHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::EditAllIndexerSettings(None)) @@ -952,9 +961,9 @@ mod tests { fn test_indexer_settings_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if INDEXER_SETTINGS_BLOCKS.contains(&active_radarr_block) { - assert!(IndexerSettingsHandler::accepts(&active_radarr_block)); + assert!(IndexerSettingsHandler::accepts(active_radarr_block)); } else { - assert!(!IndexerSettingsHandler::accepts(&active_radarr_block)); + assert!(!IndexerSettingsHandler::accepts(active_radarr_block)); } }) } @@ -965,10 +974,10 @@ mod tests { app.is_loading = true; let handler = IndexerSettingsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ); assert!(!handler.is_ready()); @@ -980,10 +989,10 @@ mod tests { app.is_loading = false; let handler = IndexerSettingsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ); assert!(!handler.is_ready()); @@ -996,10 +1005,10 @@ mod tests { app.data.radarr_data.indexer_settings = Some(IndexerSettings::default()); let handler = IndexerSettingsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::AllIndexerSettingsPrompt, - &None, + ActiveRadarrBlock::AllIndexerSettingsPrompt, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs index 5fd3b1d..5f070a7 100644 --- a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod tests { - use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; use strum::IntoEnumIterator; @@ -15,107 +14,6 @@ mod tests { use crate::models::servarr_models::Indexer; use crate::test_handler_delegation; - mod test_handle_scroll_up_and_down { - use rstest::rstest; - - use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; - - use super::*; - - test_iterable_scroll!( - test_indexers_scroll, - IndexersHandler, - indexers, - simple_stateful_iterable_vec!(Indexer, String, protocol), - ActiveRadarrBlock::Indexers, - None, - protocol - ); - - #[rstest] - fn test_indexers_scroll_no_op_when_not_ready( - #[values( - DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key - )] - key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .indexers - .set_items(simple_stateful_iterable_vec!(Indexer, String, protocol)); - - IndexersHandler::with(&key, &mut app, &ActiveRadarrBlock::Indexers, &None).handle(); - - assert_str_eq!( - app.data.radarr_data.indexers.current_selection().protocol, - "Test 1" - ); - - IndexersHandler::with(&key, &mut app, &ActiveRadarrBlock::Indexers, &None).handle(); - - assert_str_eq!( - app.data.radarr_data.indexers.current_selection().protocol, - "Test 1" - ); - } - } - - mod test_handle_home_end { - use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; - - use super::*; - - test_iterable_home_and_end!( - test_indexers_home_end, - IndexersHandler, - indexers, - extended_stateful_iterable_vec!(Indexer, String, protocol), - ActiveRadarrBlock::Indexers, - None, - protocol - ); - - #[test] - fn test_indexers_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .indexers - .set_items(extended_stateful_iterable_vec!(Indexer, String, protocol)); - - IndexersHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Indexers, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.indexers.current_selection().protocol, - "Test 1" - ); - - IndexersHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Indexers, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.indexers.current_selection().protocol, - "Test 1" - ); - } - } - mod test_handle_delete { use pretty_assertions::assert_eq; @@ -132,11 +30,11 @@ mod tests { .indexers .set_items(vec![Indexer::default()]); - IndexersHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle(); + IndexersHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::DeleteIndexerPrompt.into() + ActiveRadarrBlock::DeleteIndexerPrompt.into() ); } @@ -151,9 +49,9 @@ mod tests { .indexers .set_items(vec![Indexer::default()]); - IndexersHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle(); + IndexersHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } } @@ -170,20 +68,20 @@ mod tests { app.data.radarr_data.main_tabs.set_index(5); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); } @@ -194,18 +92,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(5); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::System.into() + ActiveRadarrBlock::System.into() ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); } #[rstest] @@ -214,23 +112,11 @@ mod tests { ) { let mut app = App::default(); - IndexersHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::DeleteIndexerPrompt, - &None, - ) - .handle(); + IndexersHandler::with(key, &mut app, ActiveRadarrBlock::DeleteIndexerPrompt, None).handle(); assert!(app.data.radarr_data.prompt_confirm); - IndexersHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::DeleteIndexerPrompt, - &None, - ) - .handle(); + IndexersHandler::with(key, &mut app, ActiveRadarrBlock::DeleteIndexerPrompt, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); } @@ -306,11 +192,11 @@ mod tests { radarr_data.indexers.set_items(vec![indexer]); app.data.radarr_data = radarr_data; - IndexersHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle(); + IndexersHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexerPrompt.into() + ActiveRadarrBlock::EditIndexerPrompt.into() ); assert_eq!( app.data.radarr_data.edit_indexer_modal, @@ -323,12 +209,12 @@ mod tests { if torrent_protocol { assert_eq!( app.data.radarr_data.selected_block.blocks, - &EDIT_INDEXER_TORRENT_SELECTION_BLOCKS + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS ); } else { assert_eq!( app.data.radarr_data.selected_block.blocks, - &EDIT_INDEXER_NZB_SELECTION_BLOCKS + EDIT_INDEXER_NZB_SELECTION_BLOCKS ); } } @@ -344,9 +230,9 @@ mod tests { .indexers .set_items(vec![Indexer::default()]); - IndexersHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle(); + IndexersHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert_eq!(app.data.radarr_data.edit_indexer_modal, None); } @@ -363,10 +249,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into()); IndexersHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::DeleteIndexerPrompt, - &None, + ActiveRadarrBlock::DeleteIndexerPrompt, + None, ) .handle(); @@ -375,7 +261,7 @@ mod tests { app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::DeleteIndexer(None)) ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } #[test] @@ -390,16 +276,16 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into()); IndexersHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::DeleteIndexerPrompt, - &None, + ActiveRadarrBlock::DeleteIndexerPrompt, + None, ) .handle(); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } } @@ -419,14 +305,14 @@ mod tests { app.data.radarr_data.prompt_confirm = true; IndexersHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::DeleteIndexerPrompt, - &None, + ActiveRadarrBlock::DeleteIndexerPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(!app.data.radarr_data.prompt_confirm); } @@ -434,14 +320,14 @@ mod tests { fn test_test_indexer_esc(#[values(true, false)] is_ready: bool) { let mut app = App::default(); app.is_loading = is_ready; - app.data.radarr_data.indexer_test_error = Some("test result".to_owned()); + app.data.radarr_data.indexer_test_errors = Some("test result".to_owned()); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::TestIndexer.into()); - IndexersHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::TestIndexer, &None).handle(); + IndexersHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::TestIndexer, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); - assert_eq!(app.data.radarr_data.indexer_test_error, None); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.data.radarr_data.indexer_test_errors, None); } #[rstest] @@ -452,9 +338,9 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); - IndexersHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle(); + IndexersHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::Indexers, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(app.error.text.is_empty()); } } @@ -469,51 +355,6 @@ mod tests { use super::*; - #[test] - fn test_indexer_add() { - let mut app = App::default(); - app - .data - .radarr_data - .indexers - .set_items(vec![Indexer::default()]); - - IndexersHandler::with( - &DEFAULT_KEYBINDINGS.add.key, - &mut app, - &ActiveRadarrBlock::Indexers, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::AddIndexer.into() - ); - } - - #[test] - fn test_indexer_add_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); - app - .data - .radarr_data - .indexers - .set_items(vec![Indexer::default()]); - - IndexersHandler::with( - &DEFAULT_KEYBINDINGS.add.key, - &mut app, - &ActiveRadarrBlock::Indexers, - &None, - ) - .handle(); - - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); - } - #[test] fn test_refresh_indexers_key() { let mut app = App::default(); @@ -525,14 +366,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(app.should_refresh); } @@ -548,14 +389,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(!app.should_refresh); } @@ -569,20 +410,20 @@ mod tests { .set_items(vec![Indexer::default()]); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.settings.key, + DEFAULT_KEYBINDINGS.settings.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AllIndexerSettingsPrompt.into() + ActiveRadarrBlock::AllIndexerSettingsPrompt.into() ); assert_eq!( app.data.radarr_data.selected_block.blocks, - &INDEXER_SETTINGS_SELECTION_BLOCKS + INDEXER_SETTINGS_SELECTION_BLOCKS ); } @@ -598,14 +439,14 @@ mod tests { .set_items(vec![Indexer::default()]); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.settings.key, + DEFAULT_KEYBINDINGS.settings.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } #[test] @@ -618,16 +459,16 @@ mod tests { .set_items(vec![Indexer::default()]); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.test.key, + DEFAULT_KEYBINDINGS.test.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::TestIndexer.into() + ActiveRadarrBlock::TestIndexer.into() ); } @@ -643,14 +484,14 @@ mod tests { .set_items(vec![Indexer::default()]); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.test.key, + DEFAULT_KEYBINDINGS.test.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } #[test] @@ -663,16 +504,16 @@ mod tests { .set_items(vec![Indexer::default()]); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.test_all.key, + DEFAULT_KEYBINDINGS.test_all.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::TestAllIndexers.into() + ActiveRadarrBlock::TestAllIndexers.into() ); } @@ -688,14 +529,14 @@ mod tests { .set_items(vec![Indexer::default()]); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.test_all.key, + DEFAULT_KEYBINDINGS.test_all.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } #[test] @@ -710,10 +551,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into()); IndexersHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::DeleteIndexerPrompt, - &None, + ActiveRadarrBlock::DeleteIndexerPrompt, + None, ) .handle(); @@ -722,7 +563,7 @@ mod tests { app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::DeleteIndexer(None)) ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } } @@ -791,9 +632,9 @@ mod tests { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if indexers_blocks.contains(&active_radarr_block) { - assert!(IndexersHandler::accepts(&active_radarr_block)); + assert!(IndexersHandler::accepts(active_radarr_block)); } else { - assert!(!IndexersHandler::accepts(&active_radarr_block)); + assert!(!IndexersHandler::accepts(active_radarr_block)); } }) } @@ -804,10 +645,10 @@ mod tests { app.is_loading = true; let handler = IndexersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ); assert!(!handler.is_ready()); @@ -819,10 +660,10 @@ mod tests { app.is_loading = false; let handler = IndexersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ); assert!(!handler.is_ready()); @@ -839,10 +680,10 @@ mod tests { .set_items(vec![Indexer::default()]); let handler = IndexersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Indexers, - &None, + ActiveRadarrBlock::Indexers, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/indexers/mod.rs b/src/handlers/radarr_handlers/indexers/mod.rs index 84d4832..282e80a 100644 --- a/src/handlers/radarr_handlers/indexers/mod.rs +++ b/src/handlers/radarr_handlers/indexers/mod.rs @@ -1,17 +1,19 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; +use crate::handle_table_events; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; use crate::handlers::radarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; +use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, }; +use crate::models::servarr_models::Indexer; use crate::models::BlockSelectionState; -use crate::models::Scrollable; use crate::network::radarr_network::RadarrEvent; mod edit_indexer_handler; @@ -23,43 +25,52 @@ mod test_all_indexers_handler; mod indexers_handler_tests; pub(super) struct IndexersHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + context: Option, +} + +impl<'a, 'b> IndexersHandler<'a, 'b> { + handle_table_events!(self, indexers, self.app.data.radarr_data.indexers, Indexer); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, 'b> { fn handle(&mut self) { - match self.active_radarr_block { - _ if EditIndexerHandler::accepts(self.active_radarr_block) => { - EditIndexerHandler::with(self.key, self.app, self.active_radarr_block, self.context) - .handle() + let indexer_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::Indexers.into()); + + if !self.handle_indexers_table_events(indexer_table_handling_config) { + match self.active_radarr_block { + _ if EditIndexerHandler::accepts(self.active_radarr_block) => { + EditIndexerHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle() + } + _ if IndexerSettingsHandler::accepts(self.active_radarr_block) => { + IndexerSettingsHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle() + } + _ if TestAllIndexersHandler::accepts(self.active_radarr_block) => { + TestAllIndexersHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle() + } + _ => self.handle_key_event(), } - _ if IndexerSettingsHandler::accepts(self.active_radarr_block) => { - IndexerSettingsHandler::with(self.key, self.app, self.active_radarr_block, self.context) - .handle() - } - _ if TestAllIndexersHandler::accepts(self.active_radarr_block) => { - TestAllIndexersHandler::with(self.key, self.app, self.active_radarr_block, self.context) - .handle() - } - _ => self.handle_key_event(), } } - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { + fn accepts(active_block: ActiveRadarrBlock) -> bool { EditIndexerHandler::accepts(active_block) || IndexerSettingsHandler::accepts(active_block) || TestAllIndexersHandler::accepts(active_block) - || INDEXERS_BLOCKS.contains(active_block) + || INDEXERS_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> IndexersHandler<'a, 'b> { IndexersHandler { key, @@ -69,7 +80,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -77,32 +88,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, !self.app.is_loading && !self.app.data.radarr_data.indexers.is_empty() } - fn handle_scroll_up(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Indexers { - self.app.data.radarr_data.indexers.scroll_up(); - } - } + fn handle_scroll_up(&mut self) {} - fn handle_scroll_down(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Indexers { - self.app.data.radarr_data.indexers.scroll_down(); - } - } + fn handle_scroll_down(&mut self) {} - fn handle_home(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Indexers { - self.app.data.radarr_data.indexers.scroll_to_top(); - } - } + fn handle_home(&mut self) {} - fn handle_end(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Indexers { - self.app.data.radarr_data.indexers.scroll_to_bottom(); - } - } + fn handle_end(&mut self) {} fn handle_delete(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Indexers { + if self.active_radarr_block == ActiveRadarrBlock::Indexers { self .app .push_navigation_stack(ActiveRadarrBlock::DeleteIndexerPrompt.into()); @@ -141,10 +136,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, .protocol; if protocol == "torrent" { self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); } else { self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_INDEXER_NZB_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); } } _ => (), @@ -159,7 +154,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, } ActiveRadarrBlock::TestIndexer => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.indexer_test_error = None; + self.app.data.radarr_data.indexer_test_errors = None; } _ => handle_clear_errors(self.app), } @@ -169,35 +164,30 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, let key = self.key; match self.active_radarr_block { ActiveRadarrBlock::Indexers => match self.key { - _ if *key == DEFAULT_KEYBINDINGS.add.key => { - self - .app - .push_navigation_stack(ActiveRadarrBlock::AddIndexer.into()); - } - _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } - _ if *key == DEFAULT_KEYBINDINGS.test.key => { + _ if key == DEFAULT_KEYBINDINGS.test.key => { self .app .push_navigation_stack(ActiveRadarrBlock::TestIndexer.into()); } - _ if *key == DEFAULT_KEYBINDINGS.test_all.key => { + _ if key == DEFAULT_KEYBINDINGS.test_all.key => { self .app .push_navigation_stack(ActiveRadarrBlock::TestAllIndexers.into()); } - _ if *key == DEFAULT_KEYBINDINGS.settings.key => { + _ if key == DEFAULT_KEYBINDINGS.settings.key => { self .app .push_navigation_stack(ActiveRadarrBlock::AllIndexerSettingsPrompt.into()); self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&INDEXER_SETTINGS_SELECTION_BLOCKS); + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); } _ => (), }, ActiveRadarrBlock::DeleteIndexerPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { + if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteIndexer(None)); diff --git a/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs b/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs index 307008a..1b4297e 100644 --- a/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs +++ b/src/handlers/radarr_handlers/indexers/test_all_indexers_handler.rs @@ -1,30 +1,58 @@ use crate::app::App; use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::KeyEventHandler; +use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; -use crate::models::Scrollable; #[cfg(test)] #[path = "test_all_indexers_handler_tests.rs"] mod test_all_indexers_handler_tests; pub(super) struct TestAllIndexersHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, +} + +impl<'a, 'b> TestAllIndexersHandler<'a, 'b> { + handle_table_events!( + self, + indexer_test_all_results, + self + .app + .data + .radarr_data + .indexer_test_all_results + .as_mut() + .unwrap(), + IndexerTestResultModalItem + ); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - active_block == &ActiveRadarrBlock::TestAllIndexers + fn handle(&mut self) { + let test_all_indexers_test_results_table_handler_config = + TableHandlingConfig::new(ActiveRadarrBlock::TestAllIndexers.into()); + + if !self.handle_indexer_test_all_results_table_events( + test_all_indexers_test_results_table_handler_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveRadarrBlock) -> bool { + active_block == ActiveRadarrBlock::TestAllIndexers } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_block: ActiveRadarrBlock, + _context: Option, ) -> TestAllIndexersHandler<'a, 'b> { TestAllIndexersHandler { key, @@ -34,7 +62,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -48,57 +76,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl !self.app.is_loading && table_is_ready } - fn handle_scroll_up(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::TestAllIndexers { - self - .app - .data - .radarr_data - .indexer_test_all_results - .as_mut() - .unwrap() - .scroll_up() - } - } + fn handle_scroll_up(&mut self) {} - fn handle_scroll_down(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::TestAllIndexers { - self - .app - .data - .radarr_data - .indexer_test_all_results - .as_mut() - .unwrap() - .scroll_down() - } - } + fn handle_scroll_down(&mut self) {} - fn handle_home(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::TestAllIndexers { - self - .app - .data - .radarr_data - .indexer_test_all_results - .as_mut() - .unwrap() - .scroll_to_top() - } - } + fn handle_home(&mut self) {} - fn handle_end(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::TestAllIndexers { - self - .app - .data - .radarr_data - .indexer_test_all_results - .as_mut() - .unwrap() - .scroll_to_bottom() - } - } + fn handle_end(&mut self) {} fn handle_delete(&mut self) {} @@ -107,7 +91,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TestAllIndexersHandl fn handle_submit(&mut self) {} fn handle_esc(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::TestAllIndexers { + if self.active_radarr_block == ActiveRadarrBlock::TestAllIndexers { self.app.pop_navigation_stack(); self.app.data.radarr_data.indexer_test_all_results = None; } diff --git a/src/handlers/radarr_handlers/indexers/test_all_indexers_handler_tests.rs b/src/handlers/radarr_handlers/indexers/test_all_indexers_handler_tests.rs index 708e22d..31a93d7 100644 --- a/src/handlers/radarr_handlers/indexers/test_all_indexers_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/test_all_indexers_handler_tests.rs @@ -2,7 +2,6 @@ mod tests { use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; - use crate::event::Key; use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::modals::IndexerTestResultModalItem; @@ -10,220 +9,6 @@ mod tests { use crate::models::stateful_table::StatefulTable; use strum::IntoEnumIterator; - mod test_handle_scroll_up_and_down { - use pretty_assertions::assert_str_eq; - use rstest::rstest; - - use crate::models::servarr_data::modals::IndexerTestResultModalItem; - use crate::models::stateful_table::StatefulTable; - use crate::simple_stateful_iterable_vec; - - use super::*; - - #[rstest] - fn test_test_all_indexers_results_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - let mut indexer_test_results = StatefulTable::default(); - indexer_test_results.set_items(simple_stateful_iterable_vec!( - IndexerTestResultModalItem, - String, - name - )); - app.data.radarr_data.indexer_test_all_results = Some(indexer_test_results); - - TestAllIndexersHandler::with(&key, &mut app, &ActiveRadarrBlock::TestAllIndexers, &None) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .indexer_test_all_results - .as_ref() - .unwrap() - .current_selection() - .name, - "Test 2" - ); - - TestAllIndexersHandler::with(&key, &mut app, &ActiveRadarrBlock::TestAllIndexers, &None) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .indexer_test_all_results - .as_ref() - .unwrap() - .current_selection() - .name, - "Test 1" - ); - } - - #[rstest] - fn test_test_all_indexers_results_scroll_no_op_when_not_ready( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - let mut indexer_test_results = StatefulTable::default(); - indexer_test_results.set_items(simple_stateful_iterable_vec!( - IndexerTestResultModalItem, - String, - name - )); - app.data.radarr_data.indexer_test_all_results = Some(indexer_test_results); - - TestAllIndexersHandler::with(&key, &mut app, &ActiveRadarrBlock::TestAllIndexers, &None) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .indexer_test_all_results - .as_ref() - .unwrap() - .current_selection() - .name, - "Test 1" - ); - - TestAllIndexersHandler::with(&key, &mut app, &ActiveRadarrBlock::TestAllIndexers, &None) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .indexer_test_all_results - .as_ref() - .unwrap() - .current_selection() - .name, - "Test 1" - ); - } - } - - mod test_handle_home_end { - use crate::extended_stateful_iterable_vec; - use crate::models::servarr_data::modals::IndexerTestResultModalItem; - use crate::models::stateful_table::StatefulTable; - use pretty_assertions::assert_str_eq; - - use super::*; - - #[test] - fn test_test_all_indexers_results_home_end() { - let mut app = App::default(); - let mut indexer_test_results = StatefulTable::default(); - indexer_test_results.set_items(extended_stateful_iterable_vec!( - IndexerTestResultModalItem, - String, - name - )); - app.data.radarr_data.indexer_test_all_results = Some(indexer_test_results); - - TestAllIndexersHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::TestAllIndexers, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .indexer_test_all_results - .as_ref() - .unwrap() - .current_selection() - .name, - "Test 3" - ); - - TestAllIndexersHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::TestAllIndexers, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .indexer_test_all_results - .as_ref() - .unwrap() - .current_selection() - .name, - "Test 1" - ); - } - - #[test] - fn test_test_all_indexers_results_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - let mut indexer_test_results = StatefulTable::default(); - indexer_test_results.set_items(extended_stateful_iterable_vec!( - IndexerTestResultModalItem, - String, - name - )); - app.data.radarr_data.indexer_test_all_results = Some(indexer_test_results); - - TestAllIndexersHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::TestAllIndexers, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .indexer_test_all_results - .as_ref() - .unwrap() - .current_selection() - .name, - "Test 1" - ); - - TestAllIndexersHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::TestAllIndexers, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .indexer_test_all_results - .as_ref() - .unwrap() - .current_selection() - .name, - "Test 1" - ); - } - } - mod test_handle_esc { use super::*; use crate::models::stateful_table::StatefulTable; @@ -239,14 +24,14 @@ mod tests { app.data.radarr_data.indexer_test_all_results = Some(StatefulTable::default()); TestAllIndexersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::TestAllIndexers, - &None, + ActiveRadarrBlock::TestAllIndexers, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); assert!(!app.data.radarr_data.prompt_confirm); assert!(app.data.radarr_data.indexer_test_all_results.is_none()); } @@ -256,9 +41,9 @@ mod tests { fn test_test_all_indexers_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if active_radarr_block == ActiveRadarrBlock::TestAllIndexers { - assert!(TestAllIndexersHandler::accepts(&active_radarr_block)); + assert!(TestAllIndexersHandler::accepts(active_radarr_block)); } else { - assert!(!TestAllIndexersHandler::accepts(&active_radarr_block)); + assert!(!TestAllIndexersHandler::accepts(active_radarr_block)); } }); } @@ -269,10 +54,10 @@ mod tests { app.is_loading = true; let handler = TestAllIndexersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::TestAllIndexers, - &None, + ActiveRadarrBlock::TestAllIndexers, + None, ); assert!(!handler.is_ready()); @@ -284,10 +69,10 @@ mod tests { app.is_loading = false; let handler = TestAllIndexersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::TestAllIndexers, - &None, + ActiveRadarrBlock::TestAllIndexers, + None, ); assert!(!handler.is_ready()); @@ -300,10 +85,10 @@ mod tests { app.data.radarr_data.indexer_test_all_results = Some(StatefulTable::default()); let handler = TestAllIndexersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::TestAllIndexers, - &None, + ActiveRadarrBlock::TestAllIndexers, + None, ); assert!(!handler.is_ready()); @@ -318,10 +103,10 @@ mod tests { app.data.radarr_data.indexer_test_all_results = Some(indexer_test_results); let handler = TestAllIndexersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::TestAllIndexers, - &None, + ActiveRadarrBlock::TestAllIndexers, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/library/add_movie_handler.rs b/src/handlers/radarr_handlers/library/add_movie_handler.rs index c47e22a..c6c11d5 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler.rs @@ -1,33 +1,59 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::AddMovieSearchResult; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, }; use crate::models::{BlockSelectionState, Scrollable}; use crate::network::radarr_network::RadarrEvent; -use crate::{handle_text_box_keys, handle_text_box_left_right_keys, App, Key}; +use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, App, Key}; #[cfg(test)] #[path = "add_movie_handler_tests.rs"] mod add_movie_handler_tests; pub(super) struct AddMovieHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + context: Option, +} + +impl<'a, 'b> AddMovieHandler<'a, 'b> { + handle_table_events!( + self, + add_movie_search_results, + self + .app + .data + .radarr_data + .add_searched_movies + .as_mut() + .unwrap(), + AddMovieSearchResult + ); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - ADD_MOVIE_BLOCKS.contains(active_block) + fn handle(&mut self) { + let add_movie_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::AddMovieSearchResults.into()); + + if !self.handle_add_movie_search_results_table_events(add_movie_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveRadarrBlock) -> bool { + ADD_MOVIE_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> AddMovieHandler<'a, 'b> { AddMovieHandler { key, @@ -37,7 +63,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -47,14 +73,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, fn handle_scroll_up(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::AddMovieSearchResults => self - .app - .data - .radarr_data - .add_searched_movies - .as_mut() - .unwrap() - .scroll_up(), ActiveRadarrBlock::AddMovieSelectMonitor => self .app .data @@ -91,21 +109,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, .unwrap() .root_folder_list .scroll_up(), - ActiveRadarrBlock::AddMoviePrompt => self.app.data.radarr_data.selected_block.previous(), + ActiveRadarrBlock::AddMoviePrompt => self.app.data.radarr_data.selected_block.up(), _ => (), } } fn handle_scroll_down(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::AddMovieSearchResults => self - .app - .data - .radarr_data - .add_searched_movies - .as_mut() - .unwrap() - .scroll_down(), ActiveRadarrBlock::AddMovieSelectMonitor => self .app .data @@ -142,21 +152,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, .unwrap() .root_folder_list .scroll_down(), - ActiveRadarrBlock::AddMoviePrompt => self.app.data.radarr_data.selected_block.next(), + ActiveRadarrBlock::AddMoviePrompt => self.app.data.radarr_data.selected_block.down(), _ => (), } } fn handle_home(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::AddMovieSearchResults => self - .app - .data - .radarr_data - .add_searched_movies - .as_mut() - .unwrap() - .scroll_to_top(), ActiveRadarrBlock::AddMovieSelectMonitor => self .app .data @@ -216,14 +218,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, fn handle_end(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::AddMovieSearchResults => self - .app - .data - .radarr_data - .add_searched_movies - .as_mut() - .unwrap() - .scroll_to_bottom(), ActiveRadarrBlock::AddMovieSelectMonitor => self .app .data @@ -313,7 +307,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, fn handle_submit(&mut self) { match self.active_radarr_block { - _ if *self.active_radarr_block == ActiveRadarrBlock::AddMovieSearchInput + _ if self.active_radarr_block == ActiveRadarrBlock::AddMovieSearchInput && !self .app .data @@ -329,7 +323,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, .push_navigation_stack(ActiveRadarrBlock::AddMovieSearchResults.into()); self.app.should_ignore_quit_key = false; } - _ if *self.active_radarr_block == ActiveRadarrBlock::AddMovieSearchResults + _ if self.active_radarr_block == ActiveRadarrBlock::AddMovieSearchResults && self.app.data.radarr_data.add_searched_movies.is_some() => { let tmdb_id = self @@ -360,7 +354,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, .push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); self.app.data.radarr_data.add_movie_modal = Some((&self.app.data.radarr_data).into()); self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); + BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS); } } ActiveRadarrBlock::AddMoviePrompt => { @@ -377,16 +371,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, | ActiveRadarrBlock::AddMovieSelectQualityProfile | ActiveRadarrBlock::AddMovieSelectRootFolder => self.app.push_navigation_stack( ( - *self.app.data.radarr_data.selected_block.get_active_block(), - *self.context, + self.app.data.radarr_data.selected_block.get_active_block(), + self.context, ) .into(), ), ActiveRadarrBlock::AddMovieTagsInput => { self.app.push_navigation_stack( ( - *self.app.data.radarr_data.selected_block.get_active_block(), - *self.context, + self.app.data.radarr_data.selected_block.get_active_block(), + self.context, ) .into(), ); @@ -463,8 +457,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for AddMovieHandler<'a, } ActiveRadarrBlock::AddMoviePrompt => { if self.app.data.radarr_data.selected_block.get_active_block() - == &ActiveRadarrBlock::AddMovieConfirmPrompt - && *key == DEFAULT_KEYBINDINGS.confirm.key + == ActiveRadarrBlock::AddMovieConfirmPrompt + && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie(None)); diff --git a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs index 2800832..ebd98b5 100644 --- a/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/add_movie_handler_tests.rs @@ -20,123 +20,11 @@ mod tests { use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::ADD_MOVIE_SELECTION_BLOCKS; - use crate::models::stateful_table::StatefulTable; use crate::models::BlockSelectionState; use crate::simple_stateful_iterable_vec; use super::*; - #[rstest] - fn test_add_movie_search_results_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - let mut add_searched_movies = StatefulTable::default(); - add_searched_movies.set_items(simple_stateful_iterable_vec!( - AddMovieSearchResult, - HorizontallyScrollableText - )); - app.data.radarr_data.add_searched_movies = Some(add_searched_movies); - - AddMovieHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 2" - ); - - AddMovieHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[rstest] - fn test_add_movie_search_results_scroll_no_op_when_not_ready( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - let mut add_searched_movies = StatefulTable::default(); - add_searched_movies.set_items(simple_stateful_iterable_vec!( - AddMovieSearchResult, - HorizontallyScrollableText - )); - app.data.radarr_data.add_searched_movies = Some(add_searched_movies); - - AddMovieHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - - AddMovieHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - #[rstest] fn test_add_movie_select_monitor_scroll( #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, @@ -156,10 +44,10 @@ mod tests { if key == Key::Up { for i in (0..monitor_vec.len()).rev() { AddMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AddMovieSelectMonitor, - &None, + ActiveRadarrBlock::AddMovieSelectMonitor, + None, ) .handle(); @@ -178,10 +66,10 @@ mod tests { } else { for i in 0..monitor_vec.len() { AddMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AddMovieSelectMonitor, - &None, + ActiveRadarrBlock::AddMovieSelectMonitor, + None, ) .handle(); @@ -219,10 +107,10 @@ mod tests { if key == Key::Up { for i in (0..minimum_availability_vec.len()).rev() { AddMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AddMovieSelectMinimumAvailability, - &None, + ActiveRadarrBlock::AddMovieSelectMinimumAvailability, + None, ) .handle(); @@ -241,10 +129,10 @@ mod tests { } else { for i in 0..minimum_availability_vec.len() { AddMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AddMovieSelectMinimumAvailability, - &None, + ActiveRadarrBlock::AddMovieSelectMinimumAvailability, + None, ) .handle(); @@ -279,10 +167,10 @@ mod tests { .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); AddMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AddMovieSelectQualityProfile, - &None, + ActiveRadarrBlock::AddMovieSelectQualityProfile, + None, ) .handle(); @@ -299,10 +187,10 @@ mod tests { ); AddMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AddMovieSelectQualityProfile, - &None, + ActiveRadarrBlock::AddMovieSelectQualityProfile, + None, ) .handle(); @@ -335,10 +223,10 @@ mod tests { .set_items(simple_stateful_iterable_vec!(RootFolder, String, path)); AddMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AddMovieSelectRootFolder, - &None, + ActiveRadarrBlock::AddMovieSelectRootFolder, + None, ) .handle(); @@ -356,10 +244,10 @@ mod tests { ); AddMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::AddMovieSelectRootFolder, - &None, + ActiveRadarrBlock::AddMovieSelectRootFolder, + None, ) .handle(); @@ -380,20 +268,20 @@ mod tests { #[rstest] fn test_add_movie_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { let mut app = App::default(); - app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + app.data.radarr_data.selected_block = BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - AddMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::AddMoviePrompt, &None).handle(); + AddMovieHandler::with(key, &mut app, ActiveRadarrBlock::AddMoviePrompt, None).handle(); if key == Key::Up { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::AddMovieSelectRootFolder + ActiveRadarrBlock::AddMovieSelectRootFolder ); } else { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::AddMovieSelectMinimumAvailability + ActiveRadarrBlock::AddMovieSelectMinimumAvailability ); } } @@ -402,14 +290,14 @@ mod tests { fn test_add_movie_prompt_scroll_no_op_when_not_ready(#[values(Key::Up, Key::Down)] key: Key) { let mut app = App::default(); app.is_loading = true; - app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + app.data.radarr_data.selected_block = BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - AddMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::AddMoviePrompt, &None).handle(); + AddMovieHandler::with(key, &mut app, ActiveRadarrBlock::AddMoviePrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::AddMovieSelectMonitor + ActiveRadarrBlock::AddMovieSelectMonitor ); } } @@ -421,117 +309,9 @@ mod tests { use crate::extended_stateful_iterable_vec; use crate::models::servarr_data::radarr::modals::AddMovieModal; - use crate::models::stateful_table::StatefulTable; use super::*; - #[test] - fn test_add_movie_search_results_home_end() { - let mut app = App::default(); - let mut add_searched_movies = StatefulTable::default(); - add_searched_movies.set_items(extended_stateful_iterable_vec!( - AddMovieSearchResult, - HorizontallyScrollableText - )); - app.data.radarr_data.add_searched_movies = Some(add_searched_movies); - - AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 3" - ); - - AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[test] - fn test_add_movie_search_results_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - let mut add_searched_movies = StatefulTable::default(); - add_searched_movies.set_items(extended_stateful_iterable_vec!( - AddMovieSearchResult, - HorizontallyScrollableText - )); - app.data.radarr_data.add_searched_movies = Some(add_searched_movies); - - AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - - AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .add_searched_movies - .as_ref() - .unwrap() - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - #[test] fn test_add_movie_select_monitor_home_end() { let monitor_vec = Vec::from_iter(MovieMonitor::iter()); @@ -547,10 +327,10 @@ mod tests { .set_items(monitor_vec.clone()); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::AddMovieSelectMonitor, - &None, + ActiveRadarrBlock::AddMovieSelectMonitor, + None, ) .handle(); @@ -567,10 +347,10 @@ mod tests { ); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::AddMovieSelectMonitor, - &None, + ActiveRadarrBlock::AddMovieSelectMonitor, + None, ) .handle(); @@ -602,10 +382,10 @@ mod tests { .set_items(minimum_availability_vec.clone()); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::AddMovieSelectMinimumAvailability, - &None, + ActiveRadarrBlock::AddMovieSelectMinimumAvailability, + None, ) .handle(); @@ -622,10 +402,10 @@ mod tests { ); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::AddMovieSelectMinimumAvailability, - &None, + ActiveRadarrBlock::AddMovieSelectMinimumAvailability, + None, ) .handle(); @@ -660,10 +440,10 @@ mod tests { ]); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::AddMovieSelectQualityProfile, - &None, + ActiveRadarrBlock::AddMovieSelectQualityProfile, + None, ) .handle(); @@ -680,10 +460,10 @@ mod tests { ); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::AddMovieSelectQualityProfile, - &None, + ActiveRadarrBlock::AddMovieSelectQualityProfile, + None, ) .handle(); @@ -714,10 +494,10 @@ mod tests { .set_items(extended_stateful_iterable_vec!(RootFolder, String, path)); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::AddMovieSelectRootFolder, - &None, + ActiveRadarrBlock::AddMovieSelectRootFolder, + None, ) .handle(); @@ -735,10 +515,10 @@ mod tests { ); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::AddMovieSelectRootFolder, - &None, + ActiveRadarrBlock::AddMovieSelectRootFolder, + None, ) .handle(); @@ -762,10 +542,10 @@ mod tests { app.data.radarr_data.add_movie_search = Some("Test".into()); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::AddMovieSearchInput, - &None, + ActiveRadarrBlock::AddMovieSearchInput, + None, ) .handle(); @@ -782,10 +562,10 @@ mod tests { ); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::AddMovieSearchInput, - &None, + ActiveRadarrBlock::AddMovieSearchInput, + None, ) .handle(); @@ -811,10 +591,10 @@ mod tests { }); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::AddMovieTagsInput, - &None, + ActiveRadarrBlock::AddMovieTagsInput, + None, ) .handle(); @@ -832,10 +612,10 @@ mod tests { ); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::AddMovieTagsInput, - &None, + ActiveRadarrBlock::AddMovieTagsInput, + None, ) .handle(); @@ -866,11 +646,11 @@ mod tests { fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { let mut app = App::default(); - AddMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::AddMoviePrompt, &None).handle(); + AddMovieHandler::with(key, &mut app, ActiveRadarrBlock::AddMoviePrompt, None).handle(); assert!(app.data.radarr_data.prompt_confirm); - AddMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::AddMoviePrompt, &None).handle(); + AddMovieHandler::with(key, &mut app, ActiveRadarrBlock::AddMoviePrompt, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); } @@ -881,10 +661,10 @@ mod tests { app.data.radarr_data.add_movie_search = Some("Test".into()); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::AddMovieSearchInput, - &None, + ActiveRadarrBlock::AddMovieSearchInput, + None, ) .handle(); @@ -901,10 +681,10 @@ mod tests { ); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::AddMovieSearchInput, - &None, + ActiveRadarrBlock::AddMovieSearchInput, + None, ) .handle(); @@ -930,10 +710,10 @@ mod tests { }); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::AddMovieTagsInput, - &None, + ActiveRadarrBlock::AddMovieTagsInput, + None, ) .handle(); @@ -951,10 +731,10 @@ mod tests { ); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::AddMovieTagsInput, - &None, + ActiveRadarrBlock::AddMovieTagsInput, + None, ) .handle(); @@ -996,17 +776,17 @@ mod tests { app.data.radarr_data.add_movie_search = Some("test".into()); AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddMovieSearchInput, - &None, + ActiveRadarrBlock::AddMovieSearchInput, + None, ) .handle(); assert!(!app.should_ignore_quit_key); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMovieSearchResults.into() + ActiveRadarrBlock::AddMovieSearchResults.into() ); } @@ -1018,17 +798,17 @@ mod tests { app.should_ignore_quit_key = true; AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddMovieSearchInput, - &None, + ActiveRadarrBlock::AddMovieSearchInput, + None, ) .handle(); assert!(app.should_ignore_quit_key); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMovieSearchInput.into() + ActiveRadarrBlock::AddMovieSearchInput.into() ); } @@ -1042,20 +822,20 @@ mod tests { BiMap::from_iter([(1, "B - Test 2".to_owned()), (0, "A - Test 1".to_owned())]); AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, + ActiveRadarrBlock::AddMovieSearchResults, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMoviePrompt.into() + ActiveRadarrBlock::AddMoviePrompt.into() ); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::AddMovieSelectRootFolder + ActiveRadarrBlock::AddMovieSelectRootFolder ); assert!(app.data.radarr_data.add_movie_modal.is_some()); assert!(!app @@ -1107,16 +887,16 @@ mod tests { add_searched_movies.set_items(vec![AddMovieSearchResult::default()]); AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, + ActiveRadarrBlock::AddMovieSearchResults, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMovieSearchResults.into() + ActiveRadarrBlock::AddMovieSearchResults.into() ); assert!(app.data.radarr_data.add_movie_modal.is_none()); } @@ -1126,16 +906,16 @@ mod tests { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchResults.into()); AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, + ActiveRadarrBlock::AddMovieSearchResults, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMovieSearchResults.into() + ActiveRadarrBlock::AddMovieSearchResults.into() ); } @@ -1152,16 +932,16 @@ mod tests { .set_items(vec![Movie::default()]); AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddMovieSearchResults, - &None, + ActiveRadarrBlock::AddMovieSearchResults, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMovieAlreadyInLibrary.into() + ActiveRadarrBlock::AddMovieAlreadyInLibrary.into() ); } @@ -1170,22 +950,22 @@ mod tests { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); - app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(ADD_MOVIE_SELECTION_BLOCKS.len() - 1); + .set_index(0, ADD_MOVIE_SELECTION_BLOCKS.len() - 1); AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddMoviePrompt, - &None, + ActiveRadarrBlock::AddMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); } @@ -1196,22 +976,22 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); app.data.radarr_data.prompt_confirm = true; - app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(ADD_MOVIE_SELECTION_BLOCKS.len() - 1); + .set_index(0, ADD_MOVIE_SELECTION_BLOCKS.len() - 1); AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddMoviePrompt, - &None, + ActiveRadarrBlock::AddMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::AddMovie(None)) @@ -1227,7 +1007,7 @@ mod tests { #[case(ActiveRadarrBlock::AddMovieTagsInput, 4)] fn test_add_movie_prompt_selected_block_submit( #[case] selected_block: ActiveRadarrBlock, - #[case] index: usize, + #[case] y_index: usize, ) { let mut app = App::default(); app.push_navigation_stack( @@ -1237,20 +1017,20 @@ mod tests { ) .into(), ); - app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(index); + app.data.radarr_data.selected_block = BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(0, y_index); AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddMoviePrompt, - &Some(ActiveRadarrBlock::CollectionDetails), + ActiveRadarrBlock::AddMoviePrompt, + Some(ActiveRadarrBlock::CollectionDetails), ) .handle(); assert_eq!( app.get_current_route(), - &(selected_block, Some(ActiveRadarrBlock::CollectionDetails)).into() + (selected_block, Some(ActiveRadarrBlock::CollectionDetails)).into() ); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); @@ -1275,16 +1055,16 @@ mod tests { app.push_navigation_stack(active_radarr_block.into()); AddMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &active_radarr_block, - &Some(ActiveRadarrBlock::CollectionDetails), + active_radarr_block, + Some(ActiveRadarrBlock::CollectionDetails), ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMoviePrompt.into() + ActiveRadarrBlock::AddMoviePrompt.into() ); if active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput { @@ -1315,15 +1095,15 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); AddMovieHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::AddMovieSearchInput, - &None, + ActiveRadarrBlock::AddMovieSearchInput, + None, ) .handle(); assert!(!app.should_ignore_quit_key); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!(app.data.radarr_data.add_movie_search, None); } @@ -1336,17 +1116,17 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::AddMovieTagsInput.into()); AddMovieHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::AddMovieTagsInput, - &None, + ActiveRadarrBlock::AddMovieTagsInput, + None, ) .handle(); assert!(!app.should_ignore_quit_key); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMoviePrompt.into() + ActiveRadarrBlock::AddMoviePrompt.into() ); } @@ -1368,11 +1148,11 @@ mod tests { )); app.data.radarr_data.add_searched_movies = Some(add_searched_movies); - AddMovieHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + AddMovieHandler::with(ESC_KEY, &mut app, active_radarr_block, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMovieSearchInput.into() + ActiveRadarrBlock::AddMovieSearchInput.into() ); assert!(app.data.radarr_data.add_searched_movies.is_none()); assert!(app.should_ignore_quit_key); @@ -1386,16 +1166,16 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::AddMovieAlreadyInLibrary.into()); AddMovieHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::AddMovieAlreadyInLibrary, - &None, + ActiveRadarrBlock::AddMovieAlreadyInLibrary, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMovieSearchResults.into() + ActiveRadarrBlock::AddMovieSearchResults.into() ); } @@ -1407,18 +1187,12 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchResults.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); - AddMovieHandler::with( - &ESC_KEY, - &mut app, - &ActiveRadarrBlock::AddMoviePrompt, - &None, - ) - .handle(); + AddMovieHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::AddMoviePrompt, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMovieSearchResults.into() + ActiveRadarrBlock::AddMovieSearchResults.into() ); assert!(app.data.radarr_data.add_movie_modal.is_none()); } @@ -1432,17 +1206,17 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::AddMovieTagsInput.into()); AddMovieHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::AddMovieTagsInput, - &None, + ActiveRadarrBlock::AddMovieTagsInput, + None, ) .handle(); assert!(!app.should_ignore_quit_key); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMoviePrompt.into() + ActiveRadarrBlock::AddMoviePrompt.into() ); } @@ -1473,16 +1247,16 @@ mod tests { ); AddMovieHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &active_radarr_block, - &Some(ActiveRadarrBlock::CollectionDetails), + active_radarr_block, + Some(ActiveRadarrBlock::CollectionDetails), ) .handle(); assert_eq!( app.get_current_route(), - &( + ( ActiveRadarrBlock::AddMoviePrompt, Some(ActiveRadarrBlock::CollectionDetails), ) @@ -1507,10 +1281,10 @@ mod tests { app.data.radarr_data.add_movie_search = Some("Test".into()); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::AddMovieSearchInput, - &None, + ActiveRadarrBlock::AddMovieSearchInput, + None, ) .handle(); @@ -1529,10 +1303,10 @@ mod tests { }); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::AddMovieTagsInput, - &None, + ActiveRadarrBlock::AddMovieTagsInput, + None, ) .handle(); @@ -1555,10 +1329,10 @@ mod tests { app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default()); AddMovieHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::AddMovieSearchInput, - &None, + ActiveRadarrBlock::AddMovieSearchInput, + None, ) .handle(); @@ -1574,10 +1348,10 @@ mod tests { app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default()); AddMovieHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::AddMovieTagsInput, - &None, + ActiveRadarrBlock::AddMovieTagsInput, + None, ) .handle(); @@ -1600,22 +1374,22 @@ mod tests { app.data.radarr_data.add_movie_modal = Some(AddMovieModal::default()); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); - app.data.radarr_data.selected_block = BlockSelectionState::new(&ADD_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(ADD_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(ADD_MOVIE_SELECTION_BLOCKS.len() - 1); + .set_index(0, ADD_MOVIE_SELECTION_BLOCKS.len() - 1); AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::AddMoviePrompt, - &None, + ActiveRadarrBlock::AddMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::AddMovie(None)) @@ -1628,9 +1402,9 @@ mod tests { fn test_add_movie_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if ADD_MOVIE_BLOCKS.contains(&active_radarr_block) { - assert!(AddMovieHandler::accepts(&active_radarr_block)); + assert!(AddMovieHandler::accepts(active_radarr_block)); } else { - assert!(!AddMovieHandler::accepts(&active_radarr_block)); + assert!(!AddMovieHandler::accepts(active_radarr_block)); } }); } @@ -1641,10 +1415,10 @@ mod tests { app.is_loading = true; let handler = AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::AddMoviePrompt, - &None, + ActiveRadarrBlock::AddMoviePrompt, + None, ); assert!(!handler.is_ready()); @@ -1656,10 +1430,10 @@ mod tests { app.is_loading = false; let handler = AddMovieHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::AddMoviePrompt, - &None, + ActiveRadarrBlock::AddMoviePrompt, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler.rs b/src/handlers/radarr_handlers/library/delete_movie_handler.rs index 2113be7..6e78552 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler.rs @@ -10,22 +10,22 @@ use crate::network::radarr_network::RadarrEvent; mod delete_movie_handler_tests; pub(super) struct DeleteMovieHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - DELETE_MOVIE_BLOCKS.contains(active_block) + fn accepts(active_block: ActiveRadarrBlock) -> bool { + DELETE_MOVIE_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_block: ActiveRadarrBlock, + _context: Option, ) -> Self { DeleteMovieHandler { key, @@ -35,7 +35,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -44,14 +44,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' } fn handle_scroll_up(&mut self) { - if *self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt { - self.app.data.radarr_data.selected_block.previous(); + if self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt { + self.app.data.radarr_data.selected_block.up(); } } fn handle_scroll_down(&mut self) { - if *self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt { - self.app.data.radarr_data.selected_block.next(); + if self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt { + self.app.data.radarr_data.selected_block.down(); } } @@ -62,13 +62,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' fn handle_delete(&mut self) {} fn handle_left_right_action(&mut self) { - if *self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt { + if self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt { handle_prompt_toggle(self.app, self.key); } } fn handle_submit(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::DeleteMoviePrompt { + if self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt { match self.app.data.radarr_data.selected_block.get_active_block() { ActiveRadarrBlock::DeleteMovieConfirmPrompt => { if self.app.data.radarr_data.prompt_confirm { @@ -94,7 +94,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' } fn handle_esc(&mut self) { - if *self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt { + if self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt { self.app.pop_navigation_stack(); self.app.data.radarr_data.reset_delete_movie_preferences(); self.app.data.radarr_data.prompt_confirm = false; @@ -102,10 +102,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for DeleteMovieHandler<' } fn handle_char_key_event(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::DeleteMoviePrompt + if self.active_radarr_block == ActiveRadarrBlock::DeleteMoviePrompt && self.app.data.radarr_data.selected_block.get_active_block() - == &ActiveRadarrBlock::DeleteMovieConfirmPrompt - && *self.key == DEFAULT_KEYBINDINGS.confirm.key + == ActiveRadarrBlock::DeleteMovieConfirmPrompt + && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteMovie(None)); diff --git a/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs index 65cda31..214aa46 100644 --- a/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/delete_movie_handler_tests.rs @@ -21,22 +21,20 @@ mod tests { #[rstest] fn test_delete_movie_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { let mut app = App::default(); - app.data.radarr_data.selected_block = - BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + app.data.radarr_data.selected_block = BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - DeleteMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::DeleteMoviePrompt, &None) - .handle(); + DeleteMovieHandler::with(key, &mut app, ActiveRadarrBlock::DeleteMoviePrompt, None).handle(); if key == Key::Up { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::DeleteMovieToggleDeleteFile + ActiveRadarrBlock::DeleteMovieToggleDeleteFile ); } else { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::DeleteMovieConfirmPrompt + ActiveRadarrBlock::DeleteMovieConfirmPrompt ); } } @@ -47,16 +45,14 @@ mod tests { ) { let mut app = App::default(); app.is_loading = true; - app.data.radarr_data.selected_block = - BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + app.data.radarr_data.selected_block = BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - DeleteMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::DeleteMoviePrompt, &None) - .handle(); + DeleteMovieHandler::with(key, &mut app, ActiveRadarrBlock::DeleteMoviePrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::DeleteMovieToggleAddListExclusion + ActiveRadarrBlock::DeleteMovieToggleAddListExclusion ); } } @@ -70,13 +66,11 @@ mod tests { fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { let mut app = App::default(); - DeleteMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::DeleteMoviePrompt, &None) - .handle(); + DeleteMovieHandler::with(key, &mut app, ActiveRadarrBlock::DeleteMoviePrompt, None).handle(); assert!(app.data.radarr_data.prompt_confirm); - DeleteMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::DeleteMoviePrompt, &None) - .handle(); + DeleteMovieHandler::with(key, &mut app, ActiveRadarrBlock::DeleteMoviePrompt, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); } @@ -98,25 +92,24 @@ mod tests { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into()); - app.data.radarr_data.selected_block = - BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(DELETE_MOVIE_SELECTION_BLOCKS.len() - 1); + .set_index(0, DELETE_MOVIE_SELECTION_BLOCKS.len() - 1); app.data.radarr_data.delete_movie_files = true; app.data.radarr_data.add_list_exclusion = true; DeleteMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::DeleteMoviePrompt, - &None, + ActiveRadarrBlock::DeleteMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.delete_movie_files); @@ -131,23 +124,22 @@ mod tests { app.data.radarr_data.prompt_confirm = true; app.data.radarr_data.delete_movie_files = true; app.data.radarr_data.add_list_exclusion = true; - app.data.radarr_data.selected_block = - BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(DELETE_MOVIE_SELECTION_BLOCKS.len() - 1); + .set_index(0, DELETE_MOVIE_SELECTION_BLOCKS.len() - 1); DeleteMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::DeleteMoviePrompt, - &None, + ActiveRadarrBlock::DeleteMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::DeleteMovie(None)) @@ -169,16 +161,16 @@ mod tests { app.data.radarr_data.add_list_exclusion = true; DeleteMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::DeleteMoviePrompt, - &None, + ActiveRadarrBlock::DeleteMoviePrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::DeleteMoviePrompt.into() + ActiveRadarrBlock::DeleteMoviePrompt.into() ); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert!(!app.should_refresh); @@ -191,30 +183,29 @@ mod tests { fn test_delete_movie_toggle_delete_files_submit() { let current_route = ActiveRadarrBlock::DeleteMoviePrompt.into(); let mut app = App::default(); - app.data.radarr_data.selected_block = - BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS); app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into()); DeleteMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::DeleteMoviePrompt, - &None, + ActiveRadarrBlock::DeleteMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.get_current_route(), current_route); assert_eq!(app.data.radarr_data.delete_movie_files, true); DeleteMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::DeleteMoviePrompt, - &None, + ActiveRadarrBlock::DeleteMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.get_current_route(), current_route); assert_eq!(app.data.radarr_data.delete_movie_files, false); } } @@ -236,14 +227,14 @@ mod tests { app.data.radarr_data.add_list_exclusion = true; DeleteMovieHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::DeleteMoviePrompt, - &None, + ActiveRadarrBlock::DeleteMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert!(!app.data.radarr_data.prompt_confirm); assert!(!app.data.radarr_data.delete_movie_files); assert!(!app.data.radarr_data.add_list_exclusion); @@ -267,23 +258,22 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into()); app.data.radarr_data.delete_movie_files = true; app.data.radarr_data.add_list_exclusion = true; - app.data.radarr_data.selected_block = - BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(DELETE_MOVIE_SELECTION_BLOCKS.len() - 1); + .set_index(0, DELETE_MOVIE_SELECTION_BLOCKS.len() - 1); DeleteMovieHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::DeleteMoviePrompt, - &None, + ActiveRadarrBlock::DeleteMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::DeleteMovie(None)) @@ -299,9 +289,9 @@ mod tests { fn test_delete_movie_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if DELETE_MOVIE_BLOCKS.contains(&active_radarr_block) { - assert!(DeleteMovieHandler::accepts(&active_radarr_block)); + assert!(DeleteMovieHandler::accepts(active_radarr_block)); } else { - assert!(!DeleteMovieHandler::accepts(&active_radarr_block)); + assert!(!DeleteMovieHandler::accepts(active_radarr_block)); } }); } @@ -312,10 +302,10 @@ mod tests { app.is_loading = true; let handler = DeleteMovieHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::DeleteMoviePrompt, - &None, + ActiveRadarrBlock::DeleteMoviePrompt, + None, ); assert!(!handler.is_ready()); @@ -327,10 +317,10 @@ mod tests { app.is_loading = false; let handler = DeleteMovieHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::DeleteMoviePrompt, - &None, + ActiveRadarrBlock::DeleteMoviePrompt, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler.rs b/src/handlers/radarr_handlers/library/edit_movie_handler.rs index 0664668..c11e908 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler.rs @@ -12,22 +12,22 @@ use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; mod edit_movie_handler_tests; pub(super) struct EditMovieHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + context: Option, } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - EDIT_MOVIE_BLOCKS.contains(active_block) + fn accepts(active_block: ActiveRadarrBlock) -> bool { + EDIT_MOVIE_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> EditMovieHandler<'a, 'b> { EditMovieHandler { key, @@ -37,7 +37,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -65,7 +65,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, .unwrap() .quality_profile_list .scroll_up(), - ActiveRadarrBlock::EditMoviePrompt => self.app.data.radarr_data.selected_block.previous(), + ActiveRadarrBlock::EditMoviePrompt => self.app.data.radarr_data.selected_block.up(), _ => (), } } @@ -90,7 +90,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, .unwrap() .quality_profile_list .scroll_down(), - ActiveRadarrBlock::EditMoviePrompt => self.app.data.radarr_data.selected_block.next(), + ActiveRadarrBlock::EditMoviePrompt => self.app.data.radarr_data.selected_block.down(), _ => (), } } @@ -231,16 +231,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, ActiveRadarrBlock::EditMovieSelectMinimumAvailability | ActiveRadarrBlock::EditMovieSelectQualityProfile => self.app.push_navigation_stack( ( - *self.app.data.radarr_data.selected_block.get_active_block(), - *self.context, + self.app.data.radarr_data.selected_block.get_active_block(), + self.context, ) .into(), ), ActiveRadarrBlock::EditMoviePathInput | ActiveRadarrBlock::EditMovieTagsInput => { self.app.push_navigation_stack( ( - *self.app.data.radarr_data.selected_block.get_active_block(), - *self.context, + self.app.data.radarr_data.selected_block.get_active_block(), + self.context, ) .into(), ); @@ -329,8 +329,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditMovieHandler<'a, } ActiveRadarrBlock::EditMoviePrompt => { if self.app.data.radarr_data.selected_block.get_active_block() - == &ActiveRadarrBlock::EditMovieConfirmPrompt - && *key == DEFAULT_KEYBINDINGS.confirm.key + == ActiveRadarrBlock::EditMovieConfirmPrompt + && key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie(None)); diff --git a/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs b/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs index ab8181d..8835d13 100644 --- a/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/edit_movie_handler_tests.rs @@ -42,10 +42,10 @@ mod tests { if key == Key::Up { for i in (0..minimum_availability_vec.len()).rev() { EditMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::EditMovieSelectMinimumAvailability, - &None, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + None, ) .handle(); @@ -64,10 +64,10 @@ mod tests { } else { for i in 0..minimum_availability_vec.len() { EditMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::EditMovieSelectMinimumAvailability, - &None, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + None, ) .handle(); @@ -102,10 +102,10 @@ mod tests { .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); EditMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::EditMovieSelectQualityProfile, - &None, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + None, ) .handle(); @@ -122,10 +122,10 @@ mod tests { ); EditMovieHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::EditMovieSelectQualityProfile, - &None, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + None, ) .handle(); @@ -146,20 +146,20 @@ mod tests { fn test_edit_movie_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { let mut app = App::default(); app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); - app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + app.data.radarr_data.selected_block = BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - EditMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::EditMoviePrompt, &None).handle(); + EditMovieHandler::with(key, &mut app, ActiveRadarrBlock::EditMoviePrompt, None).handle(); if key == Key::Up { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditMovieToggleMonitored + ActiveRadarrBlock::EditMovieToggleMonitored ); } else { assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditMovieSelectQualityProfile + ActiveRadarrBlock::EditMovieSelectQualityProfile ); } } @@ -169,14 +169,14 @@ mod tests { let mut app = App::default(); app.is_loading = true; app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); - app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.next(); + app.data.radarr_data.selected_block = BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.down(); - EditMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::EditMoviePrompt, &None).handle(); + EditMovieHandler::with(key, &mut app, ActiveRadarrBlock::EditMoviePrompt, None).handle(); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditMovieSelectMinimumAvailability + ActiveRadarrBlock::EditMovieSelectMinimumAvailability ); } } @@ -205,10 +205,10 @@ mod tests { .set_items(minimum_availability_vec.clone()); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditMovieSelectMinimumAvailability, - &None, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + None, ) .handle(); @@ -225,10 +225,10 @@ mod tests { ); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditMovieSelectMinimumAvailability, - &None, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + None, ) .handle(); @@ -263,10 +263,10 @@ mod tests { ]); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditMovieSelectQualityProfile, - &None, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + None, ) .handle(); @@ -283,10 +283,10 @@ mod tests { ); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditMovieSelectQualityProfile, - &None, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + None, ) .handle(); @@ -312,10 +312,10 @@ mod tests { }); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditMoviePathInput, - &None, + ActiveRadarrBlock::EditMoviePathInput, + None, ) .handle(); @@ -333,10 +333,10 @@ mod tests { ); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditMoviePathInput, - &None, + ActiveRadarrBlock::EditMoviePathInput, + None, ) .handle(); @@ -363,10 +363,10 @@ mod tests { }); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::EditMovieTagsInput, - &None, + ActiveRadarrBlock::EditMovieTagsInput, + None, ) .handle(); @@ -384,10 +384,10 @@ mod tests { ); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::EditMovieTagsInput, - &None, + ActiveRadarrBlock::EditMovieTagsInput, + None, ) .handle(); @@ -418,11 +418,11 @@ mod tests { fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { let mut app = App::default(); - EditMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::EditMoviePrompt, &None).handle(); + EditMovieHandler::with(key, &mut app, ActiveRadarrBlock::EditMoviePrompt, None).handle(); assert!(app.data.radarr_data.prompt_confirm); - EditMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::EditMoviePrompt, &None).handle(); + EditMovieHandler::with(key, &mut app, ActiveRadarrBlock::EditMoviePrompt, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); } @@ -436,10 +436,10 @@ mod tests { }); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::EditMoviePathInput, - &None, + ActiveRadarrBlock::EditMoviePathInput, + None, ) .handle(); @@ -457,10 +457,10 @@ mod tests { ); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::EditMoviePathInput, - &None, + ActiveRadarrBlock::EditMoviePathInput, + None, ) .handle(); @@ -487,10 +487,10 @@ mod tests { }); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::EditMovieTagsInput, - &None, + ActiveRadarrBlock::EditMovieTagsInput, + None, ) .handle(); @@ -508,10 +508,10 @@ mod tests { ); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::EditMovieTagsInput, - &None, + ActiveRadarrBlock::EditMovieTagsInput, + None, ) .handle(); @@ -535,9 +535,7 @@ mod tests { use rstest::rstest; use crate::models::servarr_data::radarr::modals::EditMovieModal; - use crate::models::servarr_data::radarr::radarr_data::{ - EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, - }; + use crate::models::servarr_data::radarr::radarr_data::EDIT_MOVIE_SELECTION_BLOCKS; use crate::models::{BlockSelectionState, Route}; use crate::network::radarr_network::RadarrEvent; @@ -557,10 +555,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditMoviePathInput.into()); EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditMoviePathInput, - &None, + ActiveRadarrBlock::EditMoviePathInput, + None, ) .handle(); @@ -576,7 +574,7 @@ mod tests { .is_empty()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditMoviePrompt.into() + ActiveRadarrBlock::EditMoviePrompt.into() ); } @@ -592,10 +590,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditMoviePathInput.into()); EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditMovieTagsInput, - &None, + ActiveRadarrBlock::EditMovieTagsInput, + None, ) .handle(); @@ -611,7 +609,7 @@ mod tests { .is_empty()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditMoviePrompt.into() + ActiveRadarrBlock::EditMoviePrompt.into() ); } @@ -621,22 +619,22 @@ mod tests { app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); - app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_MOVIE_SELECTION_BLOCKS.len() - 1); EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &None, + ActiveRadarrBlock::EditMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); } @@ -647,22 +645,22 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); app.data.radarr_data.prompt_confirm = true; - app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_MOVIE_SELECTION_BLOCKS.len() - 1); EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &None, + ActiveRadarrBlock::EditMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::EditMovie(None)) @@ -681,16 +679,16 @@ mod tests { app.data.radarr_data.prompt_confirm = true; EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &None, + ActiveRadarrBlock::EditMoviePrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditMoviePrompt.into() + ActiveRadarrBlock::EditMoviePrompt.into() ); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert!(!app.should_refresh); @@ -704,18 +702,18 @@ mod tests { )); let mut app = App::default(); app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); - app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); app.push_navigation_stack(current_route); EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &Some(ActiveRadarrBlock::Movies), + ActiveRadarrBlock::EditMoviePrompt, + Some(ActiveRadarrBlock::Movies), ) .handle(); - assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.get_current_route(), current_route); assert_eq!( app .data @@ -728,14 +726,14 @@ mod tests { ); EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &Some(ActiveRadarrBlock::Movies), + ActiveRadarrBlock::EditMoviePrompt, + Some(ActiveRadarrBlock::Movies), ) .handle(); - assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.get_current_route(), current_route); assert_eq!( app .data @@ -755,7 +753,7 @@ mod tests { #[case(ActiveRadarrBlock::EditMovieTagsInput, 4)] fn test_edit_movie_prompt_selected_block_submit( #[case] selected_block: ActiveRadarrBlock, - #[case] index: usize, + #[case] y_index: usize, ) { let mut app = App::default(); app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); @@ -766,20 +764,20 @@ mod tests { ) .into(), ); - app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(index); + app.data.radarr_data.selected_block = BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(0, y_index); EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &Some(ActiveRadarrBlock::Movies), + ActiveRadarrBlock::EditMoviePrompt, + Some(ActiveRadarrBlock::Movies), ) .handle(); assert_eq!( app.get_current_route(), - &(selected_block, Some(ActiveRadarrBlock::Movies)).into() + (selected_block, Some(ActiveRadarrBlock::Movies)).into() ); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); @@ -792,7 +790,7 @@ mod tests { #[rstest] fn test_edit_movie_prompt_selected_block_submit_no_op_when_not_ready( - #[values(1, 2, 3, 4)] index: usize, + #[values(1, 2, 3, 4)] y_index: usize, ) { let mut app = App::default(); app.is_loading = true; @@ -804,20 +802,20 @@ mod tests { ) .into(), ); - app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); - app.data.radarr_data.selected_block.set_index(index); + app.data.radarr_data.selected_block = BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(0, y_index); EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &Some(ActiveRadarrBlock::Movies), + ActiveRadarrBlock::EditMoviePrompt, + Some(ActiveRadarrBlock::Movies), ) .handle(); assert_eq!( app.get_current_route(), - &( + ( ActiveRadarrBlock::EditMoviePrompt, Some(ActiveRadarrBlock::Movies), ) @@ -843,16 +841,16 @@ mod tests { app.push_navigation_stack(active_radarr_block.into()); EditMovieHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &active_radarr_block, - &Some(ActiveRadarrBlock::Movies), + active_radarr_block, + Some(ActiveRadarrBlock::Movies), ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditMoviePrompt.into() + ActiveRadarrBlock::EditMoviePrompt.into() ); if active_radarr_block == ActiveRadarrBlock::EditMoviePathInput @@ -888,12 +886,12 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); app.push_navigation_stack(active_radarr_block.into()); - EditMovieHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + EditMovieHandler::with(ESC_KEY, &mut app, active_radarr_block, None).handle(); assert!(!app.should_ignore_quit_key); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditMoviePrompt.into() + ActiveRadarrBlock::EditMoviePrompt.into() ); } @@ -904,15 +902,9 @@ mod tests { app.data.radarr_data = create_test_radarr_data(); app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); - EditMovieHandler::with( - &ESC_KEY, - &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &None, - ) - .handle(); + EditMovieHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::EditMoviePrompt, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert!(app.data.radarr_data.edit_movie_modal.is_none()); assert!(!app.data.radarr_data.prompt_confirm); @@ -932,9 +924,9 @@ mod tests { app.data.radarr_data = create_test_radarr_data(); app.push_navigation_stack(active_radarr_block.into()); - EditMovieHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + EditMovieHandler::with(ESC_KEY, &mut app, active_radarr_block, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } } @@ -942,10 +934,7 @@ mod tests { use super::*; use crate::{ models::{ - servarr_data::radarr::{ - modals::EditMovieModal, - radarr_data::{EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS}, - }, + servarr_data::radarr::{modals::EditMovieModal, radarr_data::EDIT_MOVIE_SELECTION_BLOCKS}, BlockSelectionState, }, network::radarr_network::RadarrEvent, @@ -960,10 +949,10 @@ mod tests { }); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::EditMoviePathInput, - &None, + ActiveRadarrBlock::EditMoviePathInput, + None, ) .handle(); @@ -989,10 +978,10 @@ mod tests { }); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::EditMovieTagsInput, - &None, + ActiveRadarrBlock::EditMovieTagsInput, + None, ) .handle(); @@ -1015,10 +1004,10 @@ mod tests { app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); EditMovieHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::EditMoviePathInput, - &None, + ActiveRadarrBlock::EditMoviePathInput, + None, ) .handle(); @@ -1041,10 +1030,10 @@ mod tests { app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); EditMovieHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::EditMovieTagsInput, - &None, + ActiveRadarrBlock::EditMovieTagsInput, + None, ) .handle(); @@ -1067,22 +1056,22 @@ mod tests { app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); - app.data.radarr_data.selected_block = BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); + app.data.radarr_data.selected_block = BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); app .data .radarr_data .selected_block - .set_index(EDIT_COLLECTION_SELECTION_BLOCKS.len() - 1); + .set_index(0, EDIT_MOVIE_SELECTION_BLOCKS.len() - 1); EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &None, + ActiveRadarrBlock::EditMoviePrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::EditMovie(None)) @@ -1096,9 +1085,9 @@ mod tests { fn test_edit_movie_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if EDIT_MOVIE_BLOCKS.contains(&active_radarr_block) { - assert!(EditMovieHandler::accepts(&active_radarr_block)); + assert!(EditMovieHandler::accepts(active_radarr_block)); } else { - assert!(!EditMovieHandler::accepts(&active_radarr_block)); + assert!(!EditMovieHandler::accepts(active_radarr_block)); } }); } @@ -1109,10 +1098,10 @@ mod tests { app.is_loading = true; let handler = EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &None, + ActiveRadarrBlock::EditMoviePrompt, + None, ); assert!(!handler.is_ready()); @@ -1124,10 +1113,10 @@ mod tests { app.is_loading = false; let handler = EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &None, + ActiveRadarrBlock::EditMoviePrompt, + None, ); assert!(!handler.is_ready()); @@ -1140,10 +1129,10 @@ mod tests { app.data.radarr_data.edit_movie_modal = Some(EditMovieModal::default()); let handler = EditMovieHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::EditMoviePrompt, - &None, + ActiveRadarrBlock::EditMoviePrompt, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/library/library_handler_tests.rs b/src/handlers/radarr_handlers/library/library_handler_tests.rs index 22a4cd1..7999519 100644 --- a/src/handlers/radarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/library_handler_tests.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod tests { - use core::sync::atomic::Ordering::SeqCst; use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; use std::cmp::Ordering; @@ -17,339 +16,8 @@ mod tests { MOVIE_DETAILS_BLOCKS, }; use crate::models::servarr_models::Language; - use crate::models::stateful_table::SortOption; - use crate::models::HorizontallyScrollableText; use crate::test_handler_delegation; - mod test_handle_scroll_up_and_down { - use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; - use pretty_assertions::assert_eq; - - use super::*; - - test_iterable_scroll!( - test_movies_scroll, - LibraryHandler, - movies, - simple_stateful_iterable_vec!(Movie, HorizontallyScrollableText), - ActiveRadarrBlock::Movies, - None, - title, - to_string - ); - - #[rstest] - fn test_movies_scroll_no_op_when_not_ready( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .movies - .set_items(simple_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - - LibraryHandler::with(&key, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movies - .current_selection() - .title - .to_string(), - "Test 1" - ); - - LibraryHandler::with(&key, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movies - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[rstest] - fn test_movies_sort_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let movie_field_vec = sort_options(); - let mut app = App::default(); - app.data.radarr_data.movies.sorting(sort_options()); - - if key == Key::Up { - for i in (0..movie_field_vec.len()).rev() { - LibraryHandler::with(&key, &mut app, &ActiveRadarrBlock::MoviesSortPrompt, &None) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .sort - .as_ref() - .unwrap() - .current_selection(), - &movie_field_vec[i] - ); - } - } else { - for i in 0..movie_field_vec.len() { - LibraryHandler::with(&key, &mut app, &ActiveRadarrBlock::MoviesSortPrompt, &None) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .sort - .as_ref() - .unwrap() - .current_selection(), - &movie_field_vec[(i + 1) % movie_field_vec.len()] - ); - } - } - } - } - - mod test_handle_home_end { - use pretty_assertions::assert_eq; - - use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; - - use super::*; - - test_iterable_home_and_end!( - test_movies_home_end, - LibraryHandler, - movies, - extended_stateful_iterable_vec!(Movie, HorizontallyScrollableText), - ActiveRadarrBlock::Movies, - None, - title, - to_string - ); - - #[test] - fn test_movies_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movies - .current_selection() - .title - .to_string(), - "Test 1" - ); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movies - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[test] - fn test_movie_search_box_home_end_keys() { - let mut app = App::default(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - app.data.radarr_data.movies.search = Some("Test".into()); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::SearchMovie, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .search - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 4 - ); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::SearchMovie, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .search - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 0 - ); - } - - #[test] - fn test_movie_filter_box_home_end_keys() { - let mut app = App::default(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - app.data.radarr_data.movies.filter = Some("Test".into()); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::FilterMovies, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .filter - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 4 - ); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::FilterMovies, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .filter - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 0 - ); - } - - #[test] - fn test_movies_sort_home_end() { - let movie_field_vec = sort_options(); - let mut app = App::default(); - app.data.radarr_data.movies.sorting(sort_options()); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::MoviesSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .sort - .as_ref() - .unwrap() - .current_selection(), - &movie_field_vec[movie_field_vec.len() - 1] - ); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::MoviesSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .sort - .as_ref() - .unwrap() - .current_selection(), - &movie_field_vec[0] - ); - } - } - mod test_handle_delete { use pretty_assertions::assert_eq; @@ -377,7 +45,7 @@ mod tests { ); assert_eq!( app.data.radarr_data.selected_block.blocks, - &DELETE_MOVIE_SELECTION_BLOCKS + DELETE_MOVIE_SELECTION_BLOCKS ); } @@ -392,9 +60,9 @@ mod tests { .movies .set_items(vec![Movie::default()]); - LibraryHandler::with(&DELETE_KEY, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); + LibraryHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::Movies, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } } @@ -411,18 +79,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(0); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::System.into() + ActiveRadarrBlock::System.into() ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); } #[rstest] @@ -432,20 +100,20 @@ mod tests { app.data.radarr_data.main_tabs.set_index(0); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Collections.into() ); } @@ -456,127 +124,30 @@ mod tests { let mut app = App::default(); LibraryHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::UpdateAllMoviesPrompt, - &None, + ActiveRadarrBlock::UpdateAllMoviesPrompt, + None, ) .handle(); assert!(app.data.radarr_data.prompt_confirm); LibraryHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::UpdateAllMoviesPrompt, - &None, + ActiveRadarrBlock::UpdateAllMoviesPrompt, + None, ) .handle(); assert!(!app.data.radarr_data.prompt_confirm); } - - #[test] - fn test_movie_search_box_left_right_keys() { - let mut app = App::default(); - app.data.radarr_data.movies.search = Some("Test".into()); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.left.key, - &mut app, - &ActiveRadarrBlock::SearchMovie, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .search - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 1 - ); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.right.key, - &mut app, - &ActiveRadarrBlock::SearchMovie, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .search - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 0 - ); - } - - #[test] - fn test_movie_filter_box_left_right_keys() { - let mut app = App::default(); - app.data.radarr_data.movies.filter = Some("Test".into()); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.left.key, - &mut app, - &ActiveRadarrBlock::FilterMovies, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .filter - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 1 - ); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.right.key, - &mut app, - &ActiveRadarrBlock::FilterMovies, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movies - .filter - .as_ref() - .unwrap() - .offset - .load(SeqCst), - 0 - ); - } } mod test_handle_submit { use pretty_assertions::assert_eq; - use crate::extended_stateful_iterable_vec; use crate::network::radarr_network::RadarrEvent; use super::*; @@ -592,11 +163,11 @@ mod tests { .movies .set_items(vec![Movie::default()]); - LibraryHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); + LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Movies, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::MovieDetails.into() + ActiveRadarrBlock::MovieDetails.into() ); } @@ -611,181 +182,9 @@ mod tests { .movies .set_items(vec![Movie::default()]); - LibraryHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); + LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::Movies, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - } - - #[test] - fn test_search_movie_submit() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.movies.search = Some("Test 2".into()); - - LibraryHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::SearchMovie, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.movies.current_selection().title.text, - "Test 2" - ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - } - - #[test] - fn test_search_movie_submit_error_on_no_search_hits() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.movies.search = Some("Test 5".into()); - - LibraryHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::SearchMovie, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.movies.current_selection().title.text, - "Test 1" - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::SearchMovieError.into() - ); - } - - #[test] - fn test_search_filtered_movies_submit() { - let mut app = App::default(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); - app - .data - .radarr_data - .movies - .set_filtered_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.movies.search = Some("Test 2".into()); - - LibraryHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::SearchMovie, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.movies.current_selection().title.text, - "Test 2" - ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - } - - #[test] - fn test_filter_movies_submit() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.movies.filter = Some("Test".into()); - - LibraryHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::FilterMovies, - &None, - ) - .handle(); - - assert!(app.data.radarr_data.movies.filtered_items.is_some()); - assert!(!app.should_ignore_quit_key); - assert_eq!( - app - .data - .radarr_data - .movies - .filtered_items - .as_ref() - .unwrap() - .len(), - 3 - ); - assert_str_eq!( - app.data.radarr_data.movies.current_selection().title.text, - "Test 1" - ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - } - - #[test] - fn test_filter_movies_submit_error_on_no_filter_matches() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); - app - .data - .radarr_data - .movies - .set_items(extended_stateful_iterable_vec!( - Movie, - HorizontallyScrollableText - )); - app.data.radarr_data.movies.filter = Some("Test 5".into()); - - LibraryHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::FilterMovies, - &None, - ) - .handle(); - - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.movies.filtered_items.is_none()); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::FilterMoviesError.into() - ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } #[test] @@ -801,10 +200,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into()); LibraryHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::UpdateAllMoviesPrompt, - &None, + ActiveRadarrBlock::UpdateAllMoviesPrompt, + None, ) .handle(); @@ -813,7 +212,7 @@ mod tests { app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::UpdateAllMovies) ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } #[test] @@ -828,41 +227,16 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into()); LibraryHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::UpdateAllMoviesPrompt, - &None, + ActiveRadarrBlock::UpdateAllMoviesPrompt, + None, ) .handle(); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - } - - #[test] - fn test_movies_sort_prompt_submit() { - let mut app = App::default(); - app.data.radarr_data.movies.sort_asc = true; - app.data.radarr_data.movies.sorting(sort_options()); - app.data.radarr_data.movies.set_items(movies_vec()); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); - - let mut expected_vec = movies_vec(); - expected_vec.sort_by(|a, b| a.id.cmp(&b.id)); - expected_vec.reverse(); - - LibraryHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::MoviesSortPrompt, - &None, - ) - .handle(); - - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert_eq!(app.data.radarr_data.movies.items, expected_vec); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } } @@ -877,51 +251,6 @@ mod tests { const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; - #[rstest] - fn test_search_movie_block_esc( - #[values(ActiveRadarrBlock::SearchMovie, ActiveRadarrBlock::SearchMovieError)] - active_radarr_block: ActiveRadarrBlock, - ) { - let mut app = App::default(); - app.should_ignore_quit_key = true; - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(active_radarr_block.into()); - app.data.radarr_data = create_test_radarr_data(); - app.data.radarr_data.movies.search = Some("Test".into()); - - LibraryHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); - - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(!app.should_ignore_quit_key); - assert_eq!(app.data.radarr_data.movies.search, None); - } - - #[rstest] - fn test_filter_movies_block_esc( - #[values(ActiveRadarrBlock::FilterMovies, ActiveRadarrBlock::FilterMoviesError)] - active_radarr_block: ActiveRadarrBlock, - ) { - let mut app = App::default(); - app.should_ignore_quit_key = true; - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(active_radarr_block.into()); - app.data.radarr_data = create_test_radarr_data(); - app.data.radarr_data.movies = StatefulTable { - filter: Some("Test".into()), - filtered_items: Some(Vec::new()), - filtered_state: Some(TableState::default()), - ..StatefulTable::default() - }; - - LibraryHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); - - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(!app.should_ignore_quit_key); - assert_eq!(app.data.radarr_data.movies.filter, None); - assert_eq!(app.data.radarr_data.movies.filtered_items, None); - assert_eq!(app.data.radarr_data.movies.filtered_state, None); - } - #[test] fn test_update_all_movies_prompt_blocks_esc() { let mut app = App::default(); @@ -930,34 +259,17 @@ mod tests { app.data.radarr_data.prompt_confirm = true; LibraryHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::UpdateAllMoviesPrompt, - &None, + ActiveRadarrBlock::UpdateAllMoviesPrompt, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert!(!app.data.radarr_data.prompt_confirm); } - #[test] - fn test_movies_sort_prompt_block_esc() { - let mut app = App::default(); - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); - - LibraryHandler::with( - &ESC_KEY, - &mut app, - &ActiveRadarrBlock::MoviesSortPrompt, - &None, - ) - .handle(); - - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - } - #[rstest] fn test_default_esc(#[values(true, false)] is_ready: bool) { let mut app = App::default(); @@ -974,14 +286,10 @@ mod tests { ..StatefulTable::default() }; - LibraryHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Movies, &None).handle(); + LibraryHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::Movies, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert!(app.error.text.is_empty()); - assert_eq!(app.data.radarr_data.movies.search, None); - assert_eq!(app.data.radarr_data.movies.filter, None); - assert_eq!(app.data.radarr_data.movies.filtered_items, None); - assert_eq!(app.data.radarr_data.movies.filtered_state, None); } } @@ -1002,141 +310,6 @@ mod tests { use super::*; - #[test] - fn test_search_movies_key() { - let mut app = App::default(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.search.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::SearchMovie.into() - ); - assert!(app.should_ignore_quit_key); - assert_eq!( - app.data.radarr_data.movies.search, - Some(HorizontallyScrollableText::default()) - ); - } - - #[test] - fn test_search_movies_key_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.search.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(!app.should_ignore_quit_key); - assert_eq!(app.data.radarr_data.movies.search, None); - } - - #[test] - fn test_filter_movies_key() { - let mut app = App::default(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.filter.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::FilterMovies.into() - ); - assert!(app.should_ignore_quit_key); - assert!(app.data.radarr_data.movies.filter.is_some()); - } - - #[test] - fn test_filter_movies_key_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.filter.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(!app.should_ignore_quit_key); - assert!(app.data.radarr_data.movies.filter.is_none()); - } - - #[test] - fn test_filter_movies_key_resets_previous_filter() { - let mut app = App::default(); - app.should_ignore_quit_key = true; - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app.data.radarr_data = create_test_radarr_data(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - app.data.radarr_data.movies.filter = Some("Test".into()); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.filter.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::FilterMovies.into() - ); - assert!(app.should_ignore_quit_key); - assert_eq!( - app.data.radarr_data.movies.filter, - Some(HorizontallyScrollableText::default()) - ); - assert!(app.data.radarr_data.movies.filtered_items.is_none()); - assert!(app.data.radarr_data.movies.filtered_state.is_none()); - } - #[test] fn test_movie_add_key() { let mut app = App::default(); @@ -1147,16 +320,16 @@ mod tests { .set_items(vec![Movie::default()]); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.add.key, + DEFAULT_KEYBINDINGS.add.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddMovieSearchInput.into() + ActiveRadarrBlock::AddMovieSearchInput.into() ); assert!(app.should_ignore_quit_key); assert!(app.data.radarr_data.add_movie_search.is_some()); @@ -1174,14 +347,14 @@ mod tests { .set_items(vec![Movie::default()]); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.add.key, + DEFAULT_KEYBINDINGS.add.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert!(!app.should_ignore_quit_key); assert!(app.data.radarr_data.add_movie_search.is_none()); } @@ -1207,14 +380,14 @@ mod tests { .set_items(vec![Movie::default()]); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.edit.key, + DEFAULT_KEYBINDINGS.edit.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert!(app.data.radarr_data.edit_movie_modal.is_none()); } @@ -1228,16 +401,16 @@ mod tests { .set_items(vec![Movie::default()]); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::UpdateAllMoviesPrompt.into() + ActiveRadarrBlock::UpdateAllMoviesPrompt.into() ); } @@ -1253,14 +426,14 @@ mod tests { .set_items(vec![Movie::default()]); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } #[test] @@ -1274,14 +447,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert!(app.should_refresh); } @@ -1297,164 +470,17 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert!(!app.should_refresh); } - #[test] - fn test_search_movies_box_backspace_key() { - let mut app = App::default(); - app.data.radarr_data.movies.search = Some("Test".into()); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, - &mut app, - &ActiveRadarrBlock::SearchMovie, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.movies.search.as_ref().unwrap().text, - "Tes" - ); - } - - #[test] - fn test_filter_movies_box_backspace_key() { - let mut app = App::default(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - app.data.radarr_data.movies.filter = Some("Test".into()); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, - &mut app, - &ActiveRadarrBlock::FilterMovies, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.movies.filter.as_ref().unwrap().text, - "Tes" - ); - } - - #[test] - fn test_search_movies_box_char_key() { - let mut app = App::default(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - app.data.radarr_data.movies.search = Some(HorizontallyScrollableText::default()); - - LibraryHandler::with( - &Key::Char('h'), - &mut app, - &ActiveRadarrBlock::SearchMovie, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.movies.search.as_ref().unwrap().text, - "h" - ); - } - - #[test] - fn test_filter_movies_box_char_key() { - let mut app = App::default(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - app.data.radarr_data.movies.filter = Some(HorizontallyScrollableText::default()); - - LibraryHandler::with( - &Key::Char('h'), - &mut app, - &ActiveRadarrBlock::FilterMovies, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.movies.filter.as_ref().unwrap().text, - "h" - ); - } - - #[test] - fn test_sort_key() { - let mut app = App::default(); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.sort.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::MoviesSortPrompt.into() - ); - assert_eq!( - app.data.radarr_data.movies.sort.as_ref().unwrap().items, - movies_sorting_options() - ); - assert!(!app.data.radarr_data.movies.sort_asc); - } - - #[test] - fn test_sort_key_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); - app - .data - .radarr_data - .movies - .set_items(vec![Movie::default()]); - - LibraryHandler::with( - &DEFAULT_KEYBINDINGS.sort.key, - &mut app, - &ActiveRadarrBlock::Movies, - &None, - ) - .handle(); - - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - assert!(app.data.radarr_data.movies.sort.is_none()); - } - #[test] fn test_update_all_movies_prompt_confirm() { let mut app = App::default(); @@ -1467,10 +493,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into()); LibraryHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::UpdateAllMoviesPrompt, - &None, + ActiveRadarrBlock::UpdateAllMoviesPrompt, + None, ) .handle(); @@ -1479,7 +505,7 @@ mod tests { app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::UpdateAllMovies) ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } } @@ -1744,9 +770,9 @@ mod tests { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if library_handler_blocks.contains(&active_radarr_block) { - assert!(LibraryHandler::accepts(&active_radarr_block)); + assert!(LibraryHandler::accepts(active_radarr_block)); } else { - assert!(!LibraryHandler::accepts(&active_radarr_block)); + assert!(!LibraryHandler::accepts(active_radarr_block)); } }); } @@ -1757,10 +783,10 @@ mod tests { app.is_loading = true; let handler = LibraryHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ); assert!(!handler.is_ready()); @@ -1772,10 +798,10 @@ mod tests { app.is_loading = false; let handler = LibraryHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ); assert!(!handler.is_ready()); @@ -1792,10 +818,10 @@ mod tests { .set_items(vec![Movie::default()]); let handler = LibraryHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Movies, - &None, + ActiveRadarrBlock::Movies, + None, ); assert!(handler.is_ready()); @@ -1856,16 +882,4 @@ mod tests { }, ] } - - fn sort_options() -> Vec> { - vec![SortOption { - name: "Test 1", - cmp_fn: Some(|a, b| { - b.title - .text - .to_lowercase() - .cmp(&a.title.text.to_lowercase()) - }), - }] - } } diff --git a/src/handlers/radarr_handlers/library/mod.rs b/src/handlers/radarr_handlers/library/mod.rs index acb88b1..d116e17 100644 --- a/src/handlers/radarr_handlers/library/mod.rs +++ b/src/handlers/radarr_handlers/library/mod.rs @@ -8,14 +8,15 @@ use crate::handlers::radarr_handlers::library::edit_movie_handler::EditMovieHand use crate::handlers::radarr_handlers::library::movie_details_handler::MovieDetailsHandler; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::handle_table_events; +use crate::handlers::table_handler::TableHandlingConfig; use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, DELETE_MOVIE_SELECTION_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, LIBRARY_BLOCKS, }; use crate::models::stateful_table::SortOption; -use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable}; +use crate::models::{BlockSelectionState, HorizontallyScrollableText}; use crate::network::radarr_network::RadarrEvent; -use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; mod add_movie_handler; mod delete_movie_handler; @@ -27,46 +28,65 @@ mod movie_details_handler; mod library_handler_tests; pub(super) struct LibraryHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + context: Option, +} + +impl<'a, 'b> LibraryHandler<'a, 'b> { + handle_table_events!(self, movies, self.app.data.radarr_data.movies, Movie); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, 'b> { fn handle(&mut self) { - match self.active_radarr_block { - _ if AddMovieHandler::accepts(self.active_radarr_block) => { - AddMovieHandler::with(self.key, self.app, self.active_radarr_block, self.context).handle(); + let movie_table_handling_config = TableHandlingConfig::new(ActiveRadarrBlock::Movies.into()) + .sorting_block(ActiveRadarrBlock::MoviesSortPrompt.into()) + .sort_by_fn(|a: &Movie, b: &Movie| a.id.cmp(&b.id)) + .sort_options(movies_sorting_options()) + .searching_block(ActiveRadarrBlock::SearchMovie.into()) + .search_error_block(ActiveRadarrBlock::SearchMovieError.into()) + .search_field_fn(|movie| &movie.title.text) + .filtering_block(ActiveRadarrBlock::FilterMovies.into()) + .filter_error_block(ActiveRadarrBlock::FilterMoviesError.into()) + .filter_field_fn(|movie| &movie.title.text); + + if !self.handle_movies_table_events(movie_table_handling_config) { + match self.active_radarr_block { + _ if AddMovieHandler::accepts(self.active_radarr_block) => { + AddMovieHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle(); + } + _ if DeleteMovieHandler::accepts(self.active_radarr_block) => { + DeleteMovieHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle(); + } + _ if EditMovieHandler::accepts(self.active_radarr_block) => { + EditMovieHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle(); + } + _ if MovieDetailsHandler::accepts(self.active_radarr_block) => { + MovieDetailsHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle(); + } + _ => self.handle_key_event(), } - _ if DeleteMovieHandler::accepts(self.active_radarr_block) => { - DeleteMovieHandler::with(self.key, self.app, self.active_radarr_block, self.context) - .handle(); - } - _ if EditMovieHandler::accepts(self.active_radarr_block) => { - EditMovieHandler::with(self.key, self.app, self.active_radarr_block, self.context).handle(); - } - _ if MovieDetailsHandler::accepts(self.active_radarr_block) => { - MovieDetailsHandler::with(self.key, self.app, self.active_radarr_block, self.context) - .handle(); - } - _ => self.handle_key_event(), } } - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { + fn accepts(active_block: ActiveRadarrBlock) -> bool { AddMovieHandler::accepts(active_block) || DeleteMovieHandler::accepts(active_block) || EditMovieHandler::accepts(active_block) || MovieDetailsHandler::accepts(active_block) - || LIBRARY_BLOCKS.contains(active_block) + || LIBRARY_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> LibraryHandler<'a, 'b> { LibraryHandler { key, @@ -76,7 +96,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -84,117 +104,21 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' !self.app.is_loading && !self.app.data.radarr_data.movies.is_empty() } - fn handle_scroll_up(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_up(), - ActiveRadarrBlock::MoviesSortPrompt => self - .app - .data - .radarr_data - .movies - .sort - .as_mut() - .unwrap() - .scroll_up(), - _ => (), - } - } + fn handle_scroll_up(&mut self) {} - fn handle_scroll_down(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_down(), - ActiveRadarrBlock::MoviesSortPrompt => self - .app - .data - .radarr_data - .movies - .sort - .as_mut() - .unwrap() - .scroll_down(), - _ => (), - } - } + fn handle_scroll_down(&mut self) {} - fn handle_home(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_to_top(), - ActiveRadarrBlock::SearchMovie => { - self - .app - .data - .radarr_data - .movies - .search - .as_mut() - .unwrap() - .scroll_home(); - } - ActiveRadarrBlock::FilterMovies => { - self - .app - .data - .radarr_data - .movies - .filter - .as_mut() - .unwrap() - .scroll_home(); - } - ActiveRadarrBlock::MoviesSortPrompt => self - .app - .data - .radarr_data - .movies - .sort - .as_mut() - .unwrap() - .scroll_to_top(), - _ => (), - } - } + fn handle_home(&mut self) {} - fn handle_end(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_to_bottom(), - ActiveRadarrBlock::SearchMovie => self - .app - .data - .radarr_data - .movies - .search - .as_mut() - .unwrap() - .reset_offset(), - ActiveRadarrBlock::FilterMovies => self - .app - .data - .radarr_data - .movies - .filter - .as_mut() - .unwrap() - .reset_offset(), - ActiveRadarrBlock::MoviesSortPrompt => self - .app - .data - .radarr_data - .movies - .sort - .as_mut() - .unwrap() - .scroll_to_bottom(), - _ => (), - } - } + fn handle_end(&mut self) {} fn handle_delete(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Movies { + if self.active_radarr_block == ActiveRadarrBlock::Movies { self .app .push_navigation_stack(ActiveRadarrBlock::DeleteMoviePrompt.into()); self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&DELETE_MOVIE_SELECTION_BLOCKS); + BlockSelectionState::new(DELETE_MOVIE_SELECTION_BLOCKS); } } @@ -202,20 +126,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' match self.active_radarr_block { ActiveRadarrBlock::Movies => handle_change_tab_left_right_keys(self.app, self.key), ActiveRadarrBlock::UpdateAllMoviesPrompt => handle_prompt_toggle(self.app, self.key), - ActiveRadarrBlock::SearchMovie => { - handle_text_box_left_right_keys!( - self, - self.key, - self.app.data.radarr_data.movies.search.as_mut().unwrap() - ) - } - ActiveRadarrBlock::FilterMovies => { - handle_text_box_left_right_keys!( - self, - self.key, - self.app.data.radarr_data.movies.filter.as_mut().unwrap() - ) - } _ => (), } } @@ -225,44 +135,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' ActiveRadarrBlock::Movies => self .app .push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()), - ActiveRadarrBlock::SearchMovie => { - self.app.pop_navigation_stack(); - self.app.should_ignore_quit_key = false; - - if self.app.data.radarr_data.movies.search.is_some() { - let has_match = self - .app - .data - .radarr_data - .movies - .apply_search(|movie| &movie.title.text); - - if !has_match { - self - .app - .push_navigation_stack(ActiveRadarrBlock::SearchMovieError.into()); - } - } - } - ActiveRadarrBlock::FilterMovies => { - self.app.pop_navigation_stack(); - self.app.should_ignore_quit_key = false; - - if self.app.data.radarr_data.movies.filter.is_some() { - let has_matches = self - .app - .data - .radarr_data - .movies - .apply_filter(|movie| &movie.title.text); - - if !has_matches { - self - .app - .push_navigation_stack(ActiveRadarrBlock::FilterMoviesError.into()); - } - } - } ActiveRadarrBlock::UpdateAllMoviesPrompt => { if self.app.data.radarr_data.prompt_confirm { self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies); @@ -270,44 +142,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' self.app.pop_navigation_stack(); } - ActiveRadarrBlock::MoviesSortPrompt => { - self - .app - .data - .radarr_data - .movies - .items - .sort_by(|a, b| a.id.cmp(&b.id)); - self.app.data.radarr_data.movies.apply_sorting(); - - self.app.pop_navigation_stack(); - } _ => (), } } fn handle_esc(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterMoviesError => { - self.app.pop_navigation_stack(); - self.app.data.radarr_data.movies.reset_filter(); - self.app.should_ignore_quit_key = false; - } - ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchMovieError => { - self.app.pop_navigation_stack(); - self.app.data.radarr_data.movies.reset_search(); - self.app.should_ignore_quit_key = false; - } ActiveRadarrBlock::UpdateAllMoviesPrompt => { self.app.pop_navigation_stack(); self.app.data.radarr_data.prompt_confirm = false; } - ActiveRadarrBlock::MoviesSortPrompt => { - self.app.pop_navigation_stack(); - } _ => { - self.app.data.radarr_data.movies.reset_search(); - self.app.data.radarr_data.movies.reset_filter(); handle_clear_errors(self.app); } } @@ -317,22 +162,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' let key = self.key; match self.active_radarr_block { ActiveRadarrBlock::Movies => match self.key { - _ if *key == DEFAULT_KEYBINDINGS.search.key => { - self - .app - .push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); - self.app.data.radarr_data.movies.search = Some(HorizontallyScrollableText::default()); - self.app.should_ignore_quit_key = true; - } - _ if *key == DEFAULT_KEYBINDINGS.filter.key => { - self - .app - .push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); - self.app.data.radarr_data.movies.reset_filter(); - self.app.data.radarr_data.movies.filter = Some(HorizontallyScrollableText::default()); - self.app.should_ignore_quit_key = true; - } - _ if *key == DEFAULT_KEYBINDINGS.edit.key => { + _ if key == DEFAULT_KEYBINDINGS.edit.key => { self.app.push_navigation_stack( ( ActiveRadarrBlock::EditMoviePrompt, @@ -342,52 +172,27 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' ); self.app.data.radarr_data.edit_movie_modal = Some((&self.app.data.radarr_data).into()); self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); } - _ if *key == DEFAULT_KEYBINDINGS.add.key => { + _ if key == DEFAULT_KEYBINDINGS.add.key => { self .app .push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); self.app.data.radarr_data.add_movie_search = Some(HorizontallyScrollableText::default()); self.app.should_ignore_quit_key = true; } - _ if *key == DEFAULT_KEYBINDINGS.update.key => { + _ if key == DEFAULT_KEYBINDINGS.update.key => { self .app .push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into()); } - _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } - _ if *key == DEFAULT_KEYBINDINGS.sort.key => { - self - .app - .data - .radarr_data - .movies - .sorting(movies_sorting_options()); - self - .app - .push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); - } _ => (), }, - ActiveRadarrBlock::SearchMovie => { - handle_text_box_keys!( - self, - key, - self.app.data.radarr_data.movies.search.as_mut().unwrap() - ) - } - ActiveRadarrBlock::FilterMovies => { - handle_text_box_keys!( - self, - key, - self.app.data.radarr_data.movies.filter.as_mut().unwrap() - ) - } ActiveRadarrBlock::UpdateAllMoviesPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { + if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAllMovies); diff --git a/src/handlers/radarr_handlers/library/movie_details_handler.rs b/src/handlers/radarr_handlers/library/movie_details_handler.rs index 097599a..a8d61ac 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler.rs @@ -3,8 +3,10 @@ use serde_json::Number; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; -use crate::models::radarr_models::RadarrRelease; +use crate::models::radarr_models::{Credit, MovieHistoryItem, RadarrRelease}; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, EDIT_MOVIE_SELECTION_BLOCKS, MOVIE_DETAILS_BLOCKS, }; @@ -18,22 +20,96 @@ use crate::network::radarr_network::RadarrEvent; mod movie_details_handler_tests; pub(super) struct MovieDetailsHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, +} + +impl<'a, 'b> MovieDetailsHandler<'a, 'b> { + handle_table_events!( + self, + movie_releases, + self + .app + .data + .radarr_data + .movie_details_modal + .as_mut() + .unwrap() + .movie_releases, + RadarrRelease + ); + handle_table_events!( + self, + movie_history, + self + .app + .data + .radarr_data + .movie_details_modal + .as_mut() + .unwrap() + .movie_history, + MovieHistoryItem + ); + handle_table_events!( + self, + movie_cast, + self + .app + .data + .radarr_data + .movie_details_modal + .as_mut() + .unwrap() + .movie_cast, + Credit + ); + handle_table_events!( + self, + movie_crew, + self + .app + .data + .radarr_data + .movie_details_modal + .as_mut() + .unwrap() + .movie_crew, + Credit + ); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - MOVIE_DETAILS_BLOCKS.contains(active_block) + fn handle(&mut self) { + let movie_history_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::MovieHistory.into()); + let movie_releases_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::ManualSearch.into()) + .sorting_block(ActiveRadarrBlock::ManualSearchSortPrompt.into()) + .sort_options(releases_sorting_options()); + let movie_cast_table_handling_config = TableHandlingConfig::new(ActiveRadarrBlock::Cast.into()); + let movie_crew_table_handling_config = TableHandlingConfig::new(ActiveRadarrBlock::Crew.into()); + + if !self.handle_movie_history_table_events(movie_history_table_handling_config) + && !self.handle_movie_releases_table_events(movie_releases_table_handling_config) + && !self.handle_movie_cast_table_events(movie_cast_table_handling_config) + && !self.handle_movie_crew_table_events(movie_crew_table_handling_config) + { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveRadarrBlock) -> bool { + MOVIE_DETAILS_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_block: ActiveRadarrBlock, + _context: Option, ) -> MovieDetailsHandler<'a, 'b> { MovieDetailsHandler { key, @@ -43,7 +119,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -73,8 +149,8 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< } fn handle_scroll_up(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::MovieDetails => self + if self.active_radarr_block == ActiveRadarrBlock::MovieDetails { + self .app .data .radarr_data @@ -82,62 +158,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< .as_mut() .unwrap() .movie_details - .scroll_up(), - ActiveRadarrBlock::MovieHistory => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_history - .scroll_up(), - ActiveRadarrBlock::Cast => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_cast - .scroll_up(), - ActiveRadarrBlock::Crew => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_crew - .scroll_up(), - ActiveRadarrBlock::ManualSearch => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .scroll_up(), - ActiveRadarrBlock::ManualSearchSortPrompt => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .sort - .as_mut() - .unwrap() - .scroll_up(), - _ => (), + .scroll_up() } } fn handle_scroll_down(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::MovieDetails => self + if self.active_radarr_block == ActiveRadarrBlock::MovieDetails { + self .app .data .radarr_data @@ -145,62 +172,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< .as_mut() .unwrap() .movie_details - .scroll_down(), - ActiveRadarrBlock::MovieHistory => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_history - .scroll_down(), - ActiveRadarrBlock::Cast => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_cast - .scroll_down(), - ActiveRadarrBlock::Crew => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_crew - .scroll_down(), - ActiveRadarrBlock::ManualSearch => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .scroll_down(), - ActiveRadarrBlock::ManualSearchSortPrompt => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .sort - .as_mut() - .unwrap() - .scroll_down(), - _ => (), + .scroll_down() } } fn handle_home(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::MovieDetails => self + if self.active_radarr_block == ActiveRadarrBlock::MovieDetails { + self .app .data .radarr_data @@ -208,62 +186,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< .as_mut() .unwrap() .movie_details - .scroll_to_top(), - ActiveRadarrBlock::MovieHistory => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_history - .scroll_to_top(), - ActiveRadarrBlock::Cast => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_cast - .scroll_to_top(), - ActiveRadarrBlock::Crew => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_crew - .scroll_to_top(), - ActiveRadarrBlock::ManualSearch => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .scroll_to_top(), - ActiveRadarrBlock::ManualSearchSortPrompt => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .sort - .as_mut() - .unwrap() - .scroll_to_top(), - _ => (), + .scroll_to_top() } } fn handle_end(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::MovieDetails => self + if let ActiveRadarrBlock::MovieDetails = self.active_radarr_block { + self .app .data .radarr_data @@ -271,56 +200,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< .as_mut() .unwrap() .movie_details - .scroll_to_bottom(), - ActiveRadarrBlock::MovieHistory => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_history - .scroll_to_bottom(), - ActiveRadarrBlock::Cast => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_cast - .scroll_to_bottom(), - ActiveRadarrBlock::Crew => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_crew - .scroll_to_bottom(), - ActiveRadarrBlock::ManualSearch => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .scroll_to_bottom(), - ActiveRadarrBlock::ManualSearchSortPrompt => self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .sort - .as_mut() - .unwrap() - .scroll_to_bottom(), - _ => (), + .scroll_to_bottom() } } @@ -334,16 +214,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< | ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew | ActiveRadarrBlock::ManualSearch => match self.key { - _ if *self.key == DEFAULT_KEYBINDINGS.left.key => { + _ if self.key == DEFAULT_KEYBINDINGS.left.key => { self.app.data.radarr_data.movie_info_tabs.previous(); self.app.pop_and_push_navigation_stack( - *self.app.data.radarr_data.movie_info_tabs.get_active_route(), + self.app.data.radarr_data.movie_info_tabs.get_active_route(), ); } - _ if *self.key == DEFAULT_KEYBINDINGS.right.key => { + _ if self.key == DEFAULT_KEYBINDINGS.right.key => { self.app.data.radarr_data.movie_info_tabs.next(); self.app.pop_and_push_navigation_stack( - *self.app.data.radarr_data.movie_info_tabs.get_active_route(), + self.app.data.radarr_data.movie_info_tabs.get_active_route(), ); } _ => (), @@ -385,18 +265,6 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< self.app.pop_navigation_stack(); } - ActiveRadarrBlock::ManualSearchSortPrompt => { - self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .apply_sorting(); - self.app.pop_navigation_stack(); - } _ => (), } } @@ -414,8 +282,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< } ActiveRadarrBlock::AutomaticallySearchMoviePrompt | ActiveRadarrBlock::UpdateAndScanPrompt - | ActiveRadarrBlock::ManualSearchConfirmPrompt - | ActiveRadarrBlock::ManualSearchSortPrompt => { + | ActiveRadarrBlock::ManualSearchConfirmPrompt => { self.app.pop_navigation_stack(); self.app.data.radarr_data.prompt_confirm = false; } @@ -425,81 +292,62 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< fn handle_char_key_event(&mut self) { let key = self.key; - match *self.active_radarr_block { + match self.active_radarr_block { ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::MovieHistory | ActiveRadarrBlock::FileInfo | ActiveRadarrBlock::Cast | ActiveRadarrBlock::Crew | ActiveRadarrBlock::ManualSearch => match self.key { - _ if *key == DEFAULT_KEYBINDINGS.search.key => { + _ if key == DEFAULT_KEYBINDINGS.auto_search.key => { self .app .push_navigation_stack(ActiveRadarrBlock::AutomaticallySearchMoviePrompt.into()); } - _ if *key == DEFAULT_KEYBINDINGS.edit.key => { + _ if key == DEFAULT_KEYBINDINGS.edit.key => { self.app.push_navigation_stack( ( ActiveRadarrBlock::EditMoviePrompt, - Some(*self.active_radarr_block), + Some(self.active_radarr_block), ) .into(), ); self.app.data.radarr_data.edit_movie_modal = Some((&self.app.data.radarr_data).into()); self.app.data.radarr_data.selected_block = - BlockSelectionState::new(&EDIT_MOVIE_SELECTION_BLOCKS); + BlockSelectionState::new(EDIT_MOVIE_SELECTION_BLOCKS); } - _ if *key == DEFAULT_KEYBINDINGS.update.key => { + _ if key == DEFAULT_KEYBINDINGS.update.key => { self .app .push_navigation_stack(ActiveRadarrBlock::UpdateAndScanPrompt.into()); } - _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self .app - .pop_and_push_navigation_stack((*self.active_radarr_block).into()); - } - _ if *key == DEFAULT_KEYBINDINGS.sort.key => { - self - .app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap() - .movie_releases - .sorting(releases_sorting_options()); - self - .app - .push_navigation_stack(ActiveRadarrBlock::ManualSearchSortPrompt.into()); + .pop_and_push_navigation_stack(self.active_radarr_block.into()); } _ => (), }, - ActiveRadarrBlock::AutomaticallySearchMoviePrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { - self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::TriggerAutomaticSearch(None)); + ActiveRadarrBlock::AutomaticallySearchMoviePrompt + if key == DEFAULT_KEYBINDINGS.confirm.key => + { + self.app.data.radarr_data.prompt_confirm = true; + self.app.data.radarr_data.prompt_confirm_action = + Some(RadarrEvent::TriggerAutomaticSearch(None)); - self.app.pop_navigation_stack(); - } + self.app.pop_navigation_stack(); } - ActiveRadarrBlock::UpdateAndScanPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { - self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None)); + ActiveRadarrBlock::UpdateAndScanPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { + self.app.data.radarr_data.prompt_confirm = true; + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None)); - self.app.pop_navigation_stack(); - } + self.app.pop_navigation_stack(); } - ActiveRadarrBlock::ManualSearchConfirmPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { - self.app.data.radarr_data.prompt_confirm = true; - self.app.data.radarr_data.prompt_confirm_action = - Some(RadarrEvent::DownloadRelease(None)); + ActiveRadarrBlock::ManualSearchConfirmPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { + self.app.data.radarr_data.prompt_confirm = true; + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease(None)); - self.app.pop_navigation_stack(); - } + self.app.pop_navigation_stack(); } _ => (), } diff --git a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs index e789ad4..4f7e0ab 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs @@ -19,15 +19,12 @@ mod tests { use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS}; use crate::models::servarr_models::{Language, Quality, QualityWrapper}; - use crate::models::stateful_table::SortOption; use crate::models::{HorizontallyScrollableText, ScrollableText}; mod test_handle_scroll_up_and_down { - use pretty_assertions::{assert_eq, assert_str_eq}; - use rstest::rstest; + use pretty_assertions::assert_eq; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; - use crate::simple_stateful_iterable_vec; use super::*; @@ -40,10 +37,10 @@ mod tests { }); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.up.key, + DEFAULT_KEYBINDINGS.up.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ) .handle(); @@ -60,10 +57,10 @@ mod tests { ); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.down.key, + DEFAULT_KEYBINDINGS.down.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ) .handle(); @@ -90,10 +87,10 @@ mod tests { }); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.up.key, + DEFAULT_KEYBINDINGS.up.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ) .handle(); @@ -110,10 +107,10 @@ mod tests { ); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.down.key, + DEFAULT_KEYBINDINGS.down.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ) .handle(); @@ -129,435 +126,9 @@ mod tests { 0 ); } - - #[rstest] - fn test_movie_history_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_history - .set_items(simple_stateful_iterable_vec!( - MovieHistoryItem, - HorizontallyScrollableText, - source_title - )); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::MovieHistory, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_history - .current_selection() - .source_title - .to_string(), - "Test 2" - ); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::MovieHistory, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_history - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - } - - #[rstest] - fn test_movie_history_scroll_no_op_if_not_ready( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_history - .set_items(simple_stateful_iterable_vec!( - MovieHistoryItem, - HorizontallyScrollableText, - source_title - )); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::MovieHistory, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_history - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::MovieHistory, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_history - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - } - - #[rstest] - fn test_cast_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_cast - .set_items(simple_stateful_iterable_vec!(Credit, String, person_name)); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::Cast, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_cast - .current_selection() - .person_name, - "Test 2" - ); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::Cast, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_cast - .current_selection() - .person_name, - "Test 1" - ); - } - - #[rstest] - fn test_cast_scroll_no_op_if_not_ready( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_cast - .set_items(simple_stateful_iterable_vec!(Credit, String, person_name)); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::Cast, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_cast - .current_selection() - .person_name, - "Test 1" - ); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::Cast, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_cast - .current_selection() - .person_name, - "Test 1" - ); - } - - #[rstest] - fn test_crew_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_crew - .set_items(simple_stateful_iterable_vec!(Credit, String, person_name)); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::Crew, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_crew - .current_selection() - .person_name, - "Test 2" - ); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::Crew, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_crew - .current_selection() - .person_name, - "Test 1" - ); - } - - #[rstest] - fn test_crew_scroll_no_op_when_not_ready( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_crew - .set_items(simple_stateful_iterable_vec!(Credit, String, person_name)); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::Crew, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_crew - .current_selection() - .person_name, - "Test 1" - ); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::Crew, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_crew - .current_selection() - .person_name, - "Test 1" - ); - } - - #[rstest] - fn test_manual_search_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_releases - .set_items(simple_stateful_iterable_vec!( - RadarrRelease, - HorizontallyScrollableText - )); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::ManualSearch, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection() - .title - .to_string(), - "Test 2" - ); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::ManualSearch, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[rstest] - fn test_manual_search_scroll_no_op_when_not_ready( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_releases - .set_items(simple_stateful_iterable_vec!( - RadarrRelease, - HorizontallyScrollableText - )); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::ManualSearch, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection() - .title - .to_string(), - "Test 1" - ); - - MovieDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::ManualSearch, &None).handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[rstest] - fn test_manual_search_sort_scroll( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let release_field_vec = sort_options(); - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal.movie_releases.sorting(sort_options()); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - if key == Key::Up { - for i in (0..release_field_vec.len()).rev() { - MovieDetailsHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::ManualSearchSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .sort - .as_ref() - .unwrap() - .current_selection(), - &release_field_vec[i] - ); - } - } else { - for i in 0..release_field_vec.len() { - MovieDetailsHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::ManualSearchSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .sort - .as_ref() - .unwrap() - .current_selection(), - &release_field_vec[(i + 1) % release_field_vec.len()] - ); - } - } - } } mod test_handle_home_end { - use crate::extended_stateful_iterable_vec; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use super::*; @@ -572,10 +143,10 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(movie_details_modal); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ) .handle(); @@ -592,10 +163,10 @@ mod tests { ); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ) .handle(); @@ -623,10 +194,10 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(movie_details_modal); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ) .handle(); @@ -643,10 +214,10 @@ mod tests { ); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ) .handle(); @@ -662,503 +233,6 @@ mod tests { 0 ); } - - #[test] - fn test_movie_history_home_end() { - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_history - .set_items(extended_stateful_iterable_vec!( - MovieHistoryItem, - HorizontallyScrollableText, - source_title - )); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::MovieHistory, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_history - .current_selection() - .source_title - .to_string(), - "Test 3" - ); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::MovieHistory, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_history - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - } - - #[test] - fn test_movie_history_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_history - .set_items(extended_stateful_iterable_vec!( - MovieHistoryItem, - HorizontallyScrollableText, - source_title - )); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::MovieHistory, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_history - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::MovieHistory, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_history - .current_selection() - .source_title - .to_string(), - "Test 1" - ); - } - - #[test] - fn test_cast_home_end() { - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_cast - .set_items(extended_stateful_iterable_vec!(Credit, String, person_name)); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Cast, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_cast - .current_selection() - .person_name, - "Test 3" - ); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Cast, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_cast - .current_selection() - .person_name, - "Test 1" - ); - } - - #[test] - fn test_cast_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_cast - .set_items(extended_stateful_iterable_vec!(Credit, String, person_name)); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Cast, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_cast - .current_selection() - .person_name, - "Test 1" - ); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Cast, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_cast - .current_selection() - .person_name, - "Test 1" - ); - } - - #[test] - fn test_crew_home_end() { - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_crew - .set_items(extended_stateful_iterable_vec!(Credit, String, person_name)); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Crew, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_crew - .current_selection() - .person_name, - "Test 3" - ); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Crew, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_crew - .current_selection() - .person_name, - "Test 1" - ); - } - - #[test] - fn test_crew_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_crew - .set_items(extended_stateful_iterable_vec!(Credit, String, person_name)); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::Crew, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_crew - .current_selection() - .person_name, - "Test 1" - ); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::Crew, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_crew - .current_selection() - .person_name, - "Test 1" - ); - } - - #[test] - fn test_manual_search_home_end() { - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_releases - .set_items(extended_stateful_iterable_vec!( - RadarrRelease, - HorizontallyScrollableText - )); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::ManualSearch, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection() - .title - .to_string(), - "Test 3" - ); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::ManualSearch, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[test] - fn test_manual_search_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal - .movie_releases - .set_items(extended_stateful_iterable_vec!( - RadarrRelease, - HorizontallyScrollableText - )); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::ManualSearch, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection() - .title - .to_string(), - "Test 1" - ); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::ManualSearch, - &None, - ) - .handle(); - - assert_str_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .current_selection() - .title - .to_string(), - "Test 1" - ); - } - - #[test] - fn test_manual_search_sort_home_end() { - let release_field_vec = sort_options(); - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal.movie_releases.sorting(sort_options()); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::ManualSearchSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .sort - .as_ref() - .unwrap() - .current_selection(), - &release_field_vec[release_field_vec.len() - 1] - ); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::ManualSearchSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .sort - .as_ref() - .unwrap() - .current_selection(), - &release_field_vec[0] - ); - } } mod test_handle_left_right_action { @@ -1179,11 +253,11 @@ mod tests { ) { let mut app = App::default(); - MovieDetailsHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); + MovieDetailsHandler::with(key, &mut app, active_radarr_block, None).handle(); assert!(app.data.radarr_data.prompt_confirm); - MovieDetailsHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); + MovieDetailsHandler::with(key, &mut app, active_radarr_block, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); } @@ -1212,23 +286,21 @@ mod tests { .position(|tab_route| tab_route.route == right_block.into()) .unwrap_or_default(); - MovieDetailsHandler::with(&DEFAULT_KEYBINDINGS.left.key, &mut app, &right_block, &None) - .handle(); + MovieDetailsHandler::with(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None).handle(); assert_eq!( app.get_current_route(), app.data.radarr_data.movie_info_tabs.get_active_route() ); - assert_eq!(app.get_current_route(), &left_block.into()); + assert_eq!(app.get_current_route(), left_block.into()); - MovieDetailsHandler::with(&DEFAULT_KEYBINDINGS.right.key, &mut app, &left_block, &None) - .handle(); + MovieDetailsHandler::with(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None).handle(); assert_eq!( app.get_current_route(), app.data.radarr_data.movie_info_tabs.get_active_route() ); - assert_eq!(app.get_current_route(), &right_block.into()); + assert_eq!(app.get_current_route(), right_block.into()); } } @@ -1256,17 +328,12 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(modal); app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into()); - MovieDetailsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::ManualSearch, - &None, - ) - .handle(); + MovieDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::ManualSearch, None) + .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::ManualSearchConfirmPrompt.into() + ActiveRadarrBlock::ManualSearchConfirmPrompt.into() ); } @@ -1280,17 +347,12 @@ mod tests { }); app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into()); - MovieDetailsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::ManualSearch, - &None, - ) - .handle(); + MovieDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::ManualSearch, None) + .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::ManualSearch.into() + ActiveRadarrBlock::ManualSearch.into() ); } @@ -1320,12 +382,12 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()); app.push_navigation_stack(prompt_block.into()); - MovieDetailsHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); + MovieDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::MovieDetails.into() + ActiveRadarrBlock::MovieDetails.into() ); assert_eq!( app.data.radarr_data.prompt_confirm_action, @@ -1350,54 +412,15 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()); app.push_navigation_stack(prompt_block.into()); - MovieDetailsHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); + MovieDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::MovieDetails.into() + ActiveRadarrBlock::MovieDetails.into() ); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); } - - #[test] - fn test_manual_search_sort_prompt_submit() { - let mut app = App::default(); - let mut movie_details_modal = MovieDetailsModal::default(); - movie_details_modal.movie_releases.sort_asc = true; - movie_details_modal.movie_releases.sorting(sort_options()); - movie_details_modal.movie_releases.set_items(release_vec()); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into()); - app.push_navigation_stack(ActiveRadarrBlock::ManualSearchSortPrompt.into()); - - let mut expected_vec = release_vec(); - expected_vec.reverse(); - - MovieDetailsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::ManualSearchSortPrompt, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::ManualSearch.into() - ); - assert_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .items, - expected_vec - ); - } } mod test_handle_esc { @@ -1430,9 +453,9 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(active_radarr_block.into()); - MovieDetailsHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + MovieDetailsHandler::with(ESC_KEY, &mut app, active_radarr_block, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); assert_movie_info_tabs_reset!(app.data.radarr_data); } @@ -1441,22 +464,22 @@ mod tests { #[values( ActiveRadarrBlock::AutomaticallySearchMoviePrompt, ActiveRadarrBlock::UpdateAndScanPrompt, - ActiveRadarrBlock::ManualSearchConfirmPrompt, - ActiveRadarrBlock::ManualSearchSortPrompt + ActiveRadarrBlock::ManualSearchConfirmPrompt )] prompt_block: ActiveRadarrBlock, #[values(true, false)] is_ready: bool, ) { let mut app = App::default(); + app.data.radarr_data = create_test_radarr_data(); app.is_loading = is_ready; app.data.radarr_data.prompt_confirm = true; app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(prompt_block.into()); - MovieDetailsHandler::with(&ESC_KEY, &mut app, &prompt_block, &None).handle(); + MovieDetailsHandler::with(ESC_KEY, &mut app, prompt_block, None).handle(); assert!(!app.data.radarr_data.prompt_confirm); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } } @@ -1466,7 +489,7 @@ mod tests { use rstest::rstest; use strum::IntoEnumIterator; - use crate::handlers::radarr_handlers::library::movie_details_handler::releases_sorting_options; + use crate::models::radarr_models::RadarrRelease; use crate::models::radarr_models::{MinimumAvailability, Movie}; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; @@ -1479,7 +502,7 @@ mod tests { use super::*; #[rstest] - fn test_search_key( + fn test_auto_search_key( #[values( ActiveRadarrBlock::MovieDetails, ActiveRadarrBlock::MovieHistory, @@ -1490,8 +513,6 @@ mod tests { )] active_radarr_block: ActiveRadarrBlock, ) { - use crate::models::radarr_models::RadarrRelease; - let mut app = App::default(); let mut modal = MovieDetailsModal { movie_details: ScrollableText::with_string("Test".to_owned()), @@ -1508,21 +529,21 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(modal); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.search.key, + DEFAULT_KEYBINDINGS.auto_search.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AutomaticallySearchMoviePrompt.into() + ActiveRadarrBlock::AutomaticallySearchMoviePrompt.into() ); } #[rstest] - fn test_search_key_no_op_when_not_ready( + fn test_auto_search_key_no_op_when_not_ready( #[values( ActiveRadarrBlock::MovieDetails, ActiveRadarrBlock::MovieHistory, @@ -1542,83 +563,14 @@ mod tests { }); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.search.key, + DEFAULT_KEYBINDINGS.auto_search.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); - assert_eq!(app.get_current_route(), &active_radarr_block.into()); - } - - #[test] - fn test_sort_key() { - let mut app = App::default(); - let mut modal = MovieDetailsModal::default(); - modal.movie_releases.set_items(release_vec()); - app.data.radarr_data.movie_details_modal = Some(modal); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.sort.key, - &mut app, - &ActiveRadarrBlock::ManualSearch, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::ManualSearchSortPrompt.into() - ); - assert_eq!( - app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .sort - .as_ref() - .unwrap() - .items, - releases_sorting_options() - ); - assert!( - !app - .data - .radarr_data - .movie_details_modal - .as_ref() - .unwrap() - .movie_releases - .sort_asc - ); - } - - #[test] - fn test_sort_key_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into()); - app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { - movie_details: ScrollableText::with_string("test".to_owned()), - ..MovieDetailsModal::default() - }); - - MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.sort.key, - &mut app, - &ActiveRadarrBlock::ManualSearch, - &None, - ) - .handle(); - - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::ManualSearch.into() - ); + assert_eq!(app.get_current_route(), active_radarr_block.into()); } #[rstest] @@ -1661,14 +613,14 @@ mod tests { }); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.edit.key, + DEFAULT_KEYBINDINGS.edit.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); - assert_eq!(app.get_current_route(), &active_radarr_block.into()); + assert_eq!(app.get_current_route(), active_radarr_block.into()); assert!(app.data.radarr_data.edit_movie_modal.is_none()); } @@ -1700,16 +652,16 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(modal); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::UpdateAndScanPrompt.into() + ActiveRadarrBlock::UpdateAndScanPrompt.into() ); } @@ -1734,14 +686,14 @@ mod tests { }); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); - assert_eq!(app.get_current_route(), &active_radarr_block.into()); + assert_eq!(app.get_current_route(), active_radarr_block.into()); } #[rstest] @@ -1772,14 +724,14 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(modal); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); - assert_eq!(app.get_current_route(), &active_radarr_block.into()); + assert_eq!(app.get_current_route(), active_radarr_block.into()); assert!(app.is_routing); } @@ -1798,21 +750,22 @@ mod tests { let mut app = App::default(); app.is_loading = true; app.push_navigation_stack(active_radarr_block.into()); + app.is_routing = false; app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { movie_details: ScrollableText::with_string("test".to_owned()), ..MovieDetailsModal::default() }); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); - assert_eq!(app.get_current_route(), &active_radarr_block.into()); - assert!(app.is_routing); + assert_eq!(app.get_current_route(), active_radarr_block.into()); + assert!(!app.is_routing); } #[rstest] @@ -1841,17 +794,17 @@ mod tests { app.push_navigation_stack(prompt_block.into()); MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &prompt_block, - &None, + prompt_block, + None, ) .handle(); assert!(app.data.radarr_data.prompt_confirm); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::MovieDetails.into() + ActiveRadarrBlock::MovieDetails.into() ); assert_eq!( app.data.radarr_data.prompt_confirm_action, @@ -2025,9 +978,9 @@ mod tests { fn test_movie_details_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if MOVIE_DETAILS_BLOCKS.contains(&active_radarr_block) { - assert!(MovieDetailsHandler::accepts(&active_radarr_block)); + assert!(MovieDetailsHandler::accepts(active_radarr_block)); } else { - assert!(!MovieDetailsHandler::accepts(&active_radarr_block)); + assert!(!MovieDetailsHandler::accepts(active_radarr_block)); } }); } @@ -2062,10 +1015,10 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(modal); let handler = MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &movie_details_block, - &None, + movie_details_block, + None, ); assert!(!handler.is_ready()); @@ -2078,10 +1031,10 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default()); let handler = MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ); assert!(!handler.is_ready()); @@ -2097,10 +1050,10 @@ mod tests { }); let handler = MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::MovieDetails, - &None, + ActiveRadarrBlock::MovieDetails, + None, ); assert!(handler.is_ready()); @@ -2117,10 +1070,10 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(modal); let handler = MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::MovieHistory, - &None, + ActiveRadarrBlock::MovieHistory, + None, ); assert!(handler.is_ready()); @@ -2135,10 +1088,10 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(modal); let handler = MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Cast, - &None, + ActiveRadarrBlock::Cast, + None, ); assert!(handler.is_ready()); @@ -2153,10 +1106,10 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(modal); let handler = MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::Crew, - &None, + ActiveRadarrBlock::Crew, + None, ); assert!(handler.is_ready()); @@ -2173,10 +1126,10 @@ mod tests { app.data.radarr_data.movie_details_modal = Some(modal); let handler = MovieDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::ManualSearch, - &None, + ActiveRadarrBlock::ManualSearch, + None, ); assert!(handler.is_ready()); @@ -2240,11 +1193,4 @@ mod tests { vec![release_a, release_b, release_c] } - - fn sort_options() -> Vec> { - vec![SortOption { - name: "Test 1", - cmp_fn: Some(|a, b| a.age.cmp(&b.age)), - }] - } } diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 843f2c4..1f14d74 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -27,10 +27,10 @@ mod radarr_handler_tests; mod radarr_handler_test_utils; pub(super) struct RadarrHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + context: Option, } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b> { @@ -63,15 +63,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b } } - fn accepts(_active_block: &'a ActiveRadarrBlock) -> bool { + fn accepts(_active_block: ActiveRadarrBlock) -> bool { true } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> RadarrHandler<'a, 'b> { RadarrHandler { key, @@ -81,7 +81,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -108,16 +108,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b fn handle_char_key_event(&mut self) {} } -pub fn handle_change_tab_left_right_keys(app: &mut App<'_>, key: &Key) { +pub fn handle_change_tab_left_right_keys(app: &mut App<'_>, key: Key) { let key_ref = key; match key_ref { - _ if *key == DEFAULT_KEYBINDINGS.left.key => { + _ if key == DEFAULT_KEYBINDINGS.left.key => { app.data.radarr_data.main_tabs.previous(); - app.pop_and_push_navigation_stack(*app.data.radarr_data.main_tabs.get_active_route()); + app.pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route()); } - _ if *key == DEFAULT_KEYBINDINGS.right.key => { + _ if key == DEFAULT_KEYBINDINGS.right.key => { app.data.radarr_data.main_tabs.next(); - app.pop_and_push_navigation_stack(*app.data.radarr_data.main_tabs.get_active_route()); + app.pop_and_push_navigation_stack(app.data.radarr_data.main_tabs.get_active_route()); } _ => (), } diff --git a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs index 88d6795..b56730e 100644 --- a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs +++ b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs @@ -23,15 +23,15 @@ mod utils { }]); app.data.radarr_data = radarr_data; - $handler::with(&DEFAULT_KEYBINDINGS.edit.key, &mut app, &$block, &None).handle(); + $handler::with(DEFAULT_KEYBINDINGS.edit.key, &mut app, $block, None).handle(); assert_eq!( app.get_current_route(), - &(ActiveRadarrBlock::EditMoviePrompt, Some($context)).into() + (ActiveRadarrBlock::EditMoviePrompt, Some($context)).into() ); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditMovieToggleMonitored + ActiveRadarrBlock::EditMovieToggleMonitored ); assert_eq!( app @@ -111,7 +111,7 @@ mod utils { ); assert_eq!( app.data.radarr_data.selected_block.blocks, - &EDIT_MOVIE_SELECTION_BLOCKS + EDIT_MOVIE_SELECTION_BLOCKS ); }; } @@ -137,15 +137,15 @@ mod utils { }]); app.data.radarr_data = radarr_data; - $handler::with(&DEFAULT_KEYBINDINGS.edit.key, &mut app, &$block, &None).handle(); + $handler::with(DEFAULT_KEYBINDINGS.edit.key, &mut app, $block, None).handle(); assert_eq!( app.get_current_route(), - &(ActiveRadarrBlock::EditCollectionPrompt, Some($context)).into() + (ActiveRadarrBlock::EditCollectionPrompt, $context).into() ); assert_eq!( app.data.radarr_data.selected_block.get_active_block(), - &ActiveRadarrBlock::EditCollectionToggleMonitored + ActiveRadarrBlock::EditCollectionToggleMonitored ); assert_eq!( app @@ -224,52 +224,8 @@ mod utils { ); assert_eq!( app.data.radarr_data.selected_block.blocks, - &EDIT_COLLECTION_SELECTION_BLOCKS + EDIT_COLLECTION_SELECTION_BLOCKS ); }; } - - #[macro_export] - macro_rules! assert_delete_prompt { - ($block:expr, $expected_block:expr) => { - let mut app = App::default(); - - RadarrHandler::with(&DELETE_KEY, &mut app, &$block, &None).handle(); - - assert_eq!(app.get_current_route(), &$expected_block.into()); - }; - - ($handler:ident, $block:expr, $expected_block:expr) => { - let mut app = App::default(); - - $handler::with(&DELETE_KEY, &mut app, &$block, &None).handle(); - - assert_eq!(app.get_current_route(), &$expected_block.into()); - }; - - ($app:expr, $block:expr, $expected_block:expr) => { - RadarrHandler::with(&DELETE_KEY, &mut $app, &$block, &None).handle(); - - assert_eq!($app.get_current_route(), &$expected_block.into()); - }; - - ($handler:ident, $app:expr, $block:expr, $expected_block:expr) => { - $handler::with(&DELETE_KEY, &mut $app, &$block, &None).handle(); - - assert_eq!($app.get_current_route(), &$expected_block.into()); - }; - } - - #[macro_export] - macro_rules! assert_refresh_key { - ($handler:ident, $block:expr) => { - let mut app = App::default(); - app.push_navigation_stack($block.into()); - - $handler::with(&DEFAULT_KEYBINDINGS.refresh.key, &mut app, &$block, &None).handle(); - - assert_eq!(app.get_current_route(), &$block.into()); - assert!(app.should_refresh); - }; - } } diff --git a/src/handlers/radarr_handlers/radarr_handler_tests.rs b/src/handlers/radarr_handlers/radarr_handler_tests.rs index 5641d83..758677c 100644 --- a/src/handlers/radarr_handlers/radarr_handler_tests.rs +++ b/src/handlers/radarr_handlers/radarr_handler_tests.rs @@ -27,23 +27,23 @@ mod tests { let mut app = App::default(); app.data.radarr_data.main_tabs.set_index(index); - handle_change_tab_left_right_keys(&mut app, &DEFAULT_KEYBINDINGS.left.key); + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.key); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &left_block.into() + left_block.into() ); - assert_eq!(app.get_current_route(), &left_block.into()); + assert_eq!(app.get_current_route(), left_block.into()); app.data.radarr_data.main_tabs.set_index(index); - handle_change_tab_left_right_keys(&mut app, &DEFAULT_KEYBINDINGS.right.key); + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.key); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &right_block.into() + right_block.into() ); - assert_eq!(app.get_current_route(), &right_block.into()); + assert_eq!(app.get_current_route(), right_block.into()); } #[rstest] @@ -213,7 +213,7 @@ mod tests { #[test] fn test_radarr_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { - assert!(RadarrHandler::accepts(&active_radarr_block)); + assert!(RadarrHandler::accepts(active_radarr_block)); }) } @@ -223,10 +223,10 @@ mod tests { app.is_loading = true; let handler = RadarrHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/root_folders/mod.rs b/src/handlers/radarr_handlers/root_folders/mod.rs index ef357ae..6ffc3f0 100644 --- a/src/handlers/radarr_handlers/root_folders/mod.rs +++ b/src/handlers/radarr_handlers/root_folders/mod.rs @@ -2,33 +2,53 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_FOLDERS_BLOCKS}; -use crate::models::{HorizontallyScrollableText, Scrollable}; +use crate::models::servarr_models::RootFolder; +use crate::models::HorizontallyScrollableText; use crate::network::radarr_network::RadarrEvent; -use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; +use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys}; #[cfg(test)] #[path = "root_folders_handler_tests.rs"] mod root_folders_handler_tests; pub(super) struct RootFoldersHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, +} + +impl<'a, 'b> RootFoldersHandler<'a, 'b> { + handle_table_events!( + self, + root_folders, + self.app.data.radarr_data.root_folders, + RootFolder + ); } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - ROOT_FOLDERS_BLOCKS.contains(active_block) + fn handle(&mut self) { + let root_folder_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::RootFolders.into()); + + if !self.handle_root_folders_table_events(root_folder_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveRadarrBlock) -> bool { + ROOT_FOLDERS_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_block: ActiveRadarrBlock, + _context: Option, ) -> RootFoldersHandler<'a, 'b> { RootFoldersHandler { key, @@ -38,7 +58,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -46,50 +66,38 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' !self.app.is_loading && !self.app.data.radarr_data.root_folders.is_empty() } - fn handle_scroll_up(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::RootFolders { - self.app.data.radarr_data.root_folders.scroll_up() - } - } + fn handle_scroll_up(&mut self) {} - fn handle_scroll_down(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::RootFolders { - self.app.data.radarr_data.root_folders.scroll_down() - } - } + fn handle_scroll_down(&mut self) {} fn handle_home(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_to_top(), - ActiveRadarrBlock::AddRootFolderPrompt => self + if self.active_radarr_block == ActiveRadarrBlock::AddRootFolderPrompt { + self .app .data .radarr_data .edit_root_folder .as_mut() .unwrap() - .scroll_home(), - _ => (), + .scroll_home() } } fn handle_end(&mut self) { - match self.active_radarr_block { - ActiveRadarrBlock::RootFolders => self.app.data.radarr_data.root_folders.scroll_to_bottom(), - ActiveRadarrBlock::AddRootFolderPrompt => self + if self.active_radarr_block == ActiveRadarrBlock::AddRootFolderPrompt { + self .app .data .radarr_data .edit_root_folder .as_mut() .unwrap() - .reset_offset(), - _ => (), + .reset_offset() } } fn handle_delete(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::RootFolders { + if self.active_radarr_block == ActiveRadarrBlock::RootFolders { self .app .push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into()) @@ -121,7 +129,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' self.app.pop_navigation_stack(); } - _ if *self.active_radarr_block == ActiveRadarrBlock::AddRootFolderPrompt + _ if self.active_radarr_block == ActiveRadarrBlock::AddRootFolderPrompt && !self .app .data @@ -161,10 +169,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' let key = self.key; match self.active_radarr_block { ActiveRadarrBlock::RootFolders => match self.key { - _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } - _ if *key == DEFAULT_KEYBINDINGS.add.key => { + _ if key == DEFAULT_KEYBINDINGS.add.key => { self .app .push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into()); @@ -181,7 +189,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RootFoldersHandler<' ) } ActiveRadarrBlock::DeleteRootFolderPrompt => { - if *key == DEFAULT_KEYBINDINGS.confirm.key { + if key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteRootFolder(None)); diff --git a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs index fc9bd65..420204a 100644 --- a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod tests { - use pretty_assertions::assert_str_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; @@ -12,108 +11,12 @@ mod tests { use crate::models::servarr_models::RootFolder; use crate::models::HorizontallyScrollableText; - mod test_handle_scroll_up_and_down { - use rstest::rstest; - - use crate::models::servarr_models::RootFolder; - use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; - - use super::*; - - test_iterable_scroll!( - test_root_folders_scroll, - RootFoldersHandler, - root_folders, - simple_stateful_iterable_vec!(RootFolder, String, path), - ActiveRadarrBlock::RootFolders, - None, - path - ); - - #[rstest] - fn test_root_folders_scroll_no_op_when_not_ready( - #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, - ) { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .root_folders - .set_items(simple_stateful_iterable_vec!(RootFolder, String, path)); - - RootFoldersHandler::with(&key, &mut app, &ActiveRadarrBlock::RootFolders, &None).handle(); - - assert_str_eq!( - app.data.radarr_data.root_folders.current_selection().path, - "Test 1" - ); - - RootFoldersHandler::with(&key, &mut app, &ActiveRadarrBlock::RootFolders, &None).handle(); - - assert_str_eq!( - app.data.radarr_data.root_folders.current_selection().path, - "Test 1" - ); - } - } - mod test_handle_home_end { + use pretty_assertions::assert_eq; use std::sync::atomic::Ordering; - use pretty_assertions::assert_eq; - - use crate::models::servarr_models::RootFolder; - use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; - use super::*; - - test_iterable_home_and_end!( - test_root_folders_home_end, - RootFoldersHandler, - root_folders, - extended_stateful_iterable_vec!(RootFolder, String, path), - ActiveRadarrBlock::RootFolders, - None, - path - ); - - #[test] - fn test_root_folders_home_end_no_op_when_not_ready() { - let mut app = App::default(); - app.is_loading = true; - app - .data - .radarr_data - .root_folders - .set_items(extended_stateful_iterable_vec!(RootFolder, String, path)); - - RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.end.key, - &mut app, - &ActiveRadarrBlock::RootFolders, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.root_folders.current_selection().path, - "Test 1" - ); - - RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.home.key, - &mut app, - &ActiveRadarrBlock::RootFolders, - &None, - ) - .handle(); - - assert_str_eq!( - app.data.radarr_data.root_folders.current_selection().path, - "Test 1" - ); - } + use crate::models::servarr_models::RootFolder; #[test] fn test_add_root_folder_prompt_home_end_keys() { @@ -126,10 +29,10 @@ mod tests { app.data.radarr_data.edit_root_folder = Some("Test".into()); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::AddRootFolderPrompt, - &None, + ActiveRadarrBlock::AddRootFolderPrompt, + None, ) .handle(); @@ -146,10 +49,10 @@ mod tests { ); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::AddRootFolderPrompt, - &None, + ActiveRadarrBlock::AddRootFolderPrompt, + None, ) .handle(); @@ -183,17 +86,11 @@ mod tests { .root_folders .set_items(vec![RootFolder::default()]); - RootFoldersHandler::with( - &DELETE_KEY, - &mut app, - &ActiveRadarrBlock::RootFolders, - &None, - ) - .handle(); + RootFoldersHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::RootFolders, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::DeleteRootFolderPrompt.into() + ActiveRadarrBlock::DeleteRootFolderPrompt.into() ); } @@ -208,17 +105,11 @@ mod tests { .root_folders .set_items(vec![RootFolder::default()]); - RootFoldersHandler::with( - &DELETE_KEY, - &mut app, - &ActiveRadarrBlock::RootFolders, - &None, - ) - .handle(); + RootFoldersHandler::with(DELETE_KEY, &mut app, ActiveRadarrBlock::RootFolders, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); } } @@ -238,21 +129,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(4); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::RootFolders, - &None, + ActiveRadarrBlock::RootFolders, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Blocklist.into() - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Blocklist.into() + ActiveRadarrBlock::Blocklist.into() ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Blocklist.into()); } #[rstest] @@ -262,18 +150,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(4); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::RootFolders, - &None, + ActiveRadarrBlock::RootFolders, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Indexers.into() + ActiveRadarrBlock::Indexers.into() ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } #[rstest] @@ -283,20 +171,20 @@ mod tests { let mut app = App::default(); RootFoldersHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::DeleteRootFolderPrompt, - &None, + ActiveRadarrBlock::DeleteRootFolderPrompt, + None, ) .handle(); assert!(app.data.radarr_data.prompt_confirm); RootFoldersHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::DeleteRootFolderPrompt, - &None, + ActiveRadarrBlock::DeleteRootFolderPrompt, + None, ) .handle(); @@ -309,10 +197,10 @@ mod tests { app.data.radarr_data.edit_root_folder = Some("Test".into()); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::AddRootFolderPrompt, - &None, + ActiveRadarrBlock::AddRootFolderPrompt, + None, ) .handle(); @@ -329,10 +217,10 @@ mod tests { ); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::AddRootFolderPrompt, - &None, + ActiveRadarrBlock::AddRootFolderPrompt, + None, ) .handle(); @@ -374,10 +262,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into()); RootFoldersHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddRootFolderPrompt, - &None, + ActiveRadarrBlock::AddRootFolderPrompt, + None, ) .handle(); @@ -389,7 +277,7 @@ mod tests { ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); } @@ -403,10 +291,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::AddRootFolderPrompt.into()); RootFoldersHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::AddRootFolderPrompt, - &None, + ActiveRadarrBlock::AddRootFolderPrompt, + None, ) .handle(); @@ -415,7 +303,7 @@ mod tests { assert!(app.data.radarr_data.prompt_confirm_action.is_none()); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddRootFolderPrompt.into() + ActiveRadarrBlock::AddRootFolderPrompt.into() ); } @@ -432,10 +320,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into()); RootFoldersHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::DeleteRootFolderPrompt, - &None, + ActiveRadarrBlock::DeleteRootFolderPrompt, + None, ) .handle(); @@ -446,7 +334,7 @@ mod tests { ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); } @@ -462,10 +350,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into()); RootFoldersHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::DeleteRootFolderPrompt, - &None, + ActiveRadarrBlock::DeleteRootFolderPrompt, + None, ) .handle(); @@ -473,7 +361,7 @@ mod tests { assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); } } @@ -493,16 +381,16 @@ mod tests { app.data.radarr_data.prompt_confirm = true; RootFoldersHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::DeleteRootFolderPrompt, - &None, + ActiveRadarrBlock::DeleteRootFolderPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); assert!(!app.data.radarr_data.prompt_confirm); } @@ -516,16 +404,16 @@ mod tests { app.should_ignore_quit_key = true; RootFoldersHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::AddRootFolderPrompt, - &None, + ActiveRadarrBlock::AddRootFolderPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); assert!(app.data.radarr_data.edit_root_folder.is_none()); @@ -541,11 +429,11 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); - RootFoldersHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::RootFolders, &None).handle(); + RootFoldersHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::RootFolders, None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); assert!(app.error.text.is_empty()); } @@ -568,16 +456,16 @@ mod tests { .set_items(vec![RootFolder::default()]); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.add.key, + DEFAULT_KEYBINDINGS.add.key, &mut app, - &ActiveRadarrBlock::RootFolders, - &None, + ActiveRadarrBlock::RootFolders, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::AddRootFolderPrompt.into() + ActiveRadarrBlock::AddRootFolderPrompt.into() ); assert!(app.should_ignore_quit_key); assert!(app.data.radarr_data.edit_root_folder.is_some()); @@ -595,16 +483,16 @@ mod tests { .set_items(vec![RootFolder::default()]); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.add.key, + DEFAULT_KEYBINDINGS.add.key, &mut app, - &ActiveRadarrBlock::RootFolders, - &None, + ActiveRadarrBlock::RootFolders, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); assert!(!app.should_ignore_quit_key); assert!(app.data.radarr_data.edit_root_folder.is_none()); @@ -621,16 +509,16 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::RootFolders, - &None, + ActiveRadarrBlock::RootFolders, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); assert!(app.should_refresh); } @@ -647,16 +535,16 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::RootFolders.into()); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::RootFolders, - &None, + ActiveRadarrBlock::RootFolders, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); assert!(!app.should_refresh); } @@ -672,10 +560,10 @@ mod tests { app.data.radarr_data.edit_root_folder = Some("/nfs/test".into()); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.backspace.key, + DEFAULT_KEYBINDINGS.backspace.key, &mut app, - &ActiveRadarrBlock::AddRootFolderPrompt, - &None, + ActiveRadarrBlock::AddRootFolderPrompt, + None, ) .handle(); @@ -696,10 +584,10 @@ mod tests { app.data.radarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); RootFoldersHandler::with( - &Key::Char('h'), + Key::Char('h'), &mut app, - &ActiveRadarrBlock::AddRootFolderPrompt, - &None, + ActiveRadarrBlock::AddRootFolderPrompt, + None, ) .handle(); @@ -721,10 +609,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::DeleteRootFolderPrompt.into()); RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::DeleteRootFolderPrompt, - &None, + ActiveRadarrBlock::DeleteRootFolderPrompt, + None, ) .handle(); @@ -735,7 +623,7 @@ mod tests { ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::RootFolders.into() ); } } @@ -744,9 +632,9 @@ mod tests { fn test_root_folders_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if ROOT_FOLDERS_BLOCKS.contains(&active_radarr_block) { - assert!(RootFoldersHandler::accepts(&active_radarr_block)); + assert!(RootFoldersHandler::accepts(active_radarr_block)); } else { - assert!(!RootFoldersHandler::accepts(&active_radarr_block)); + assert!(!RootFoldersHandler::accepts(active_radarr_block)); } }) } @@ -757,10 +645,10 @@ mod tests { app.is_loading = true; let handler = RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::RootFolders, - &None, + ActiveRadarrBlock::RootFolders, + None, ); assert!(!handler.is_ready()); @@ -772,10 +660,10 @@ mod tests { app.is_loading = false; let handler = RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::RootFolders, - &None, + ActiveRadarrBlock::RootFolders, + None, ); assert!(!handler.is_ready()); @@ -792,10 +680,10 @@ mod tests { .root_folders .set_items(vec![RootFolder::default()]); let handler = RootFoldersHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::RootFolders, - &None, + ActiveRadarrBlock::RootFolders, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/system/mod.rs b/src/handlers/radarr_handlers/system/mod.rs index e015915..bf94dd9 100644 --- a/src/handlers/radarr_handlers/system/mod.rs +++ b/src/handlers/radarr_handlers/system/mod.rs @@ -14,10 +14,10 @@ mod system_details_handler; mod system_handler_tests; pub(super) struct SystemHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + context: Option, } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b> { @@ -31,15 +31,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b } } - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - SystemDetailsHandler::accepts(active_block) || active_block == &ActiveRadarrBlock::System + fn accepts(active_block: ActiveRadarrBlock) -> bool { + SystemDetailsHandler::accepts(active_block) || active_block == ActiveRadarrBlock::System } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> SystemHandler<'a, 'b> { SystemHandler { key, @@ -49,7 +49,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } @@ -71,7 +71,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b fn handle_delete(&mut self) {} fn handle_left_right_action(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::System { + if self.active_radarr_block == ActiveRadarrBlock::System { handle_change_tab_left_right_keys(self.app, self.key); } } @@ -83,18 +83,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b } fn handle_char_key_event(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::System { + if self.active_radarr_block == ActiveRadarrBlock::System { let key = self.key; match self.key { - _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } - _ if *key == DEFAULT_KEYBINDINGS.events.key => { + _ if key == DEFAULT_KEYBINDINGS.events.key => { self .app .push_navigation_stack(ActiveRadarrBlock::SystemQueuedEvents.into()); } - _ if *key == DEFAULT_KEYBINDINGS.logs.key => { + _ if key == DEFAULT_KEYBINDINGS.logs.key => { self .app .push_navigation_stack(ActiveRadarrBlock::SystemLogs.into()); @@ -106,12 +106,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemHandler<'a, 'b .set_items(self.app.data.radarr_data.logs.items.to_vec()); self.app.data.radarr_data.log_details.scroll_to_bottom(); } - _ if *key == DEFAULT_KEYBINDINGS.tasks.key => { + _ if key == DEFAULT_KEYBINDINGS.tasks.key => { self .app .push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); } - _ if *key == DEFAULT_KEYBINDINGS.update.key => { + _ if key == DEFAULT_KEYBINDINGS.update.key => { self .app .push_navigation_stack(ActiveRadarrBlock::SystemUpdates.into()); diff --git a/src/handlers/radarr_handlers/system/system_details_handler.rs b/src/handlers/radarr_handlers/system/system_details_handler.rs index 3cbe7c9..d6f19dd 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler.rs @@ -12,22 +12,22 @@ use crate::network::radarr_network::RadarrEvent; mod system_details_handler_tests; pub(super) struct SystemDetailsHandler<'a, 'b> { - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_radarr_block: &'a ActiveRadarrBlock, - _context: &'a Option, + active_radarr_block: ActiveRadarrBlock, + _context: Option, } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler<'a, 'b> { - fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - SYSTEM_DETAILS_BLOCKS.contains(active_block) + fn accepts(active_block: ActiveRadarrBlock) -> bool { + SYSTEM_DETAILS_BLOCKS.contains(&active_block) } fn with( - key: &'a Key, + key: Key, app: &'a mut App<'b>, - active_block: &'a ActiveRadarrBlock, - context: &'a Option, + active_block: ActiveRadarrBlock, + context: Option, ) -> SystemDetailsHandler<'a, 'b> { SystemDetailsHandler { key, @@ -37,13 +37,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler } } - fn get_key(&self) -> &Key { + fn get_key(&self) -> Key { self.key } fn is_ready(&self) -> bool { !self.app.is_loading && (!self.app.data.radarr_data.log_details.is_empty() + || !self.app.data.radarr_data.tasks.is_empty() || !self.app.data.radarr_data.updates.is_empty()) } @@ -100,7 +101,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler match self.active_radarr_block { ActiveRadarrBlock::SystemLogs => match self.key { - _ if *key == DEFAULT_KEYBINDINGS.left.key => { + _ if key == DEFAULT_KEYBINDINGS.left.key => { self .app .data @@ -110,7 +111,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler .iter() .for_each(|log| log.scroll_right()); } - _ if *key == DEFAULT_KEYBINDINGS.right.key => { + _ if key == DEFAULT_KEYBINDINGS.right.key => { self .app .data @@ -163,14 +164,14 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for SystemDetailsHandler } fn handle_char_key_event(&mut self) { - if SYSTEM_DETAILS_BLOCKS.contains(self.active_radarr_block) - && self.key == &DEFAULT_KEYBINDINGS.refresh.key + if SYSTEM_DETAILS_BLOCKS.contains(&self.active_radarr_block) + && self.key == DEFAULT_KEYBINDINGS.refresh.key { self.app.should_refresh = true; } - if self.active_radarr_block == &ActiveRadarrBlock::SystemTaskStartConfirmPrompt - && *self.key == DEFAULT_KEYBINDINGS.confirm.key + if self.active_radarr_block == ActiveRadarrBlock::SystemTaskStartConfirmPrompt + && self.key == DEFAULT_KEYBINDINGS.confirm.key { self.app.data.radarr_data.prompt_confirm = true; self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::StartTask(None)); diff --git a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs index 65c9497..34c97da 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs @@ -26,6 +26,7 @@ mod tests { test_iterable_scroll!( test_log_details_scroll, SystemDetailsHandler, + radarr_data, log_details, simple_stateful_iterable_vec!(HorizontallyScrollableText, String, text), ActiveRadarrBlock::SystemLogs, @@ -49,14 +50,14 @@ mod tests { text )); - SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemLogs, &None).handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemLogs, None).handle(); assert_str_eq!( app.data.radarr_data.log_details.current_selection().text, "Test 1" ); - SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemLogs, &None).handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemLogs, None).handle(); assert_str_eq!( app.data.radarr_data.log_details.current_selection().text, @@ -76,14 +77,14 @@ mod tests { .tasks .set_items(simple_stateful_iterable_vec!(RadarrTask, String, name)); - SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemTasks, None).handle(); assert_str_eq!( app.data.radarr_data.tasks.current_selection().name, "Test 2" ); - SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemTasks, None).handle(); assert_str_eq!( app.data.radarr_data.tasks.current_selection().name, @@ -104,14 +105,14 @@ mod tests { .tasks .set_items(simple_stateful_iterable_vec!(RadarrTask, String, name)); - SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemTasks, None).handle(); assert_str_eq!( app.data.radarr_data.tasks.current_selection().name, "Test 1" ); - SystemDetailsHandler::with(&key, &mut app, &ActiveRadarrBlock::SystemTasks, &None).handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemTasks, None).handle(); assert_str_eq!( app.data.radarr_data.tasks.current_selection().name, @@ -131,26 +132,16 @@ mod tests { .queued_events .set_items(simple_stateful_iterable_vec!(QueueEvent, String, name)); - SystemDetailsHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::SystemQueuedEvents, - &None, - ) - .handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemQueuedEvents, None) + .handle(); assert_str_eq!( app.data.radarr_data.queued_events.current_selection().name, "Test 2" ); - SystemDetailsHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::SystemQueuedEvents, - &None, - ) - .handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemQueuedEvents, None) + .handle(); assert_str_eq!( app.data.radarr_data.queued_events.current_selection().name, @@ -171,26 +162,16 @@ mod tests { .queued_events .set_items(simple_stateful_iterable_vec!(QueueEvent, String, name)); - SystemDetailsHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::SystemQueuedEvents, - &None, - ) - .handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemQueuedEvents, None) + .handle(); assert_str_eq!( app.data.radarr_data.queued_events.current_selection().name, "Test 1" ); - SystemDetailsHandler::with( - &key, - &mut app, - &ActiveRadarrBlock::SystemQueuedEvents, - &None, - ) - .handle(); + SystemDetailsHandler::with(key, &mut app, ActiveRadarrBlock::SystemQueuedEvents, None) + .handle(); assert_str_eq!( app.data.radarr_data.queued_events.current_selection().name, @@ -204,20 +185,20 @@ mod tests { app.data.radarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.up.key, + DEFAULT_KEYBINDINGS.up.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ) .handle(); assert_eq!(app.data.radarr_data.updates.offset, 0); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.down.key, + DEFAULT_KEYBINDINGS.down.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ) .handle(); @@ -231,20 +212,20 @@ mod tests { app.data.radarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.up.key, + DEFAULT_KEYBINDINGS.up.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ) .handle(); assert_eq!(app.data.radarr_data.updates.offset, 0); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.down.key, + DEFAULT_KEYBINDINGS.down.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ) .handle(); @@ -261,6 +242,7 @@ mod tests { test_iterable_home_and_end!( test_log_details_home_end, SystemDetailsHandler, + radarr_data, log_details, extended_stateful_iterable_vec!(HorizontallyScrollableText, String, text), ActiveRadarrBlock::SystemLogs, @@ -283,10 +265,10 @@ mod tests { )); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::SystemLogs, - &None, + ActiveRadarrBlock::SystemLogs, + None, ) .handle(); @@ -296,10 +278,10 @@ mod tests { ); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::SystemLogs, - &None, + ActiveRadarrBlock::SystemLogs, + None, ) .handle(); @@ -321,10 +303,10 @@ mod tests { .set_items(extended_stateful_iterable_vec!(RadarrTask, String, name)); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::SystemTasks, - &None, + ActiveRadarrBlock::SystemTasks, + None, ) .handle(); @@ -334,10 +316,10 @@ mod tests { ); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::SystemTasks, - &None, + ActiveRadarrBlock::SystemTasks, + None, ) .handle(); @@ -360,10 +342,10 @@ mod tests { .set_items(extended_stateful_iterable_vec!(RadarrTask, String, name)); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::SystemTasks, - &None, + ActiveRadarrBlock::SystemTasks, + None, ) .handle(); @@ -373,10 +355,10 @@ mod tests { ); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::SystemTasks, - &None, + ActiveRadarrBlock::SystemTasks, + None, ) .handle(); @@ -398,10 +380,10 @@ mod tests { .set_items(extended_stateful_iterable_vec!(QueueEvent, String, name)); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::SystemQueuedEvents, - &None, + ActiveRadarrBlock::SystemQueuedEvents, + None, ) .handle(); @@ -411,10 +393,10 @@ mod tests { ); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::SystemQueuedEvents, - &None, + ActiveRadarrBlock::SystemQueuedEvents, + None, ) .handle(); @@ -437,10 +419,10 @@ mod tests { .set_items(extended_stateful_iterable_vec!(QueueEvent, String, name)); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::SystemQueuedEvents, - &None, + ActiveRadarrBlock::SystemQueuedEvents, + None, ) .handle(); @@ -450,10 +432,10 @@ mod tests { ); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::SystemQueuedEvents, - &None, + ActiveRadarrBlock::SystemQueuedEvents, + None, ) .handle(); @@ -469,20 +451,20 @@ mod tests { app.data.radarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ) .handle(); assert_eq!(app.data.radarr_data.updates.offset, 1); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ) .handle(); @@ -496,20 +478,20 @@ mod tests { app.data.radarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.end.key, + DEFAULT_KEYBINDINGS.end.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ) .handle(); assert_eq!(app.data.radarr_data.updates.offset, 0); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.home.key, + DEFAULT_KEYBINDINGS.home.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ) .handle(); @@ -534,10 +516,10 @@ mod tests { .set_items(vec!["t1".into(), "t22".into()]); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); @@ -545,10 +527,10 @@ mod tests { assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "t22"); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); @@ -556,10 +538,10 @@ mod tests { assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "22"); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); @@ -567,10 +549,10 @@ mod tests { assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "2"); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); @@ -578,10 +560,10 @@ mod tests { assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), ""); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); @@ -589,10 +571,10 @@ mod tests { assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), ""); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); @@ -600,10 +582,10 @@ mod tests { assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "2"); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); @@ -611,10 +593,10 @@ mod tests { assert_eq!(app.data.radarr_data.log_details.items[1].to_string(), "22"); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); @@ -629,20 +611,20 @@ mod tests { let mut app = App::default(); SystemDetailsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, - &None, + ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + None, ) .handle(); assert!(app.data.radarr_data.prompt_confirm); SystemDetailsHandler::with( - &key, + key, &mut app, - &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, - &None, + ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + None, ) .handle(); @@ -664,17 +646,12 @@ mod tests { let mut app = App::default(); app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned()); - SystemDetailsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::SystemTasks, - &None, - ) - .handle(); + SystemDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::SystemTasks, None) + .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into() + ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into() ); } @@ -685,17 +662,12 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned()); - SystemDetailsHandler::with( - &SUBMIT_KEY, - &mut app, - &ActiveRadarrBlock::SystemTasks, - &None, - ) - .handle(); + SystemDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::SystemTasks, None) + .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemTasks.into() + ActiveRadarrBlock::SystemTasks.into() ); } @@ -708,10 +680,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into()); SystemDetailsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, - &None, + ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + None, ) .handle(); @@ -722,7 +694,7 @@ mod tests { ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemTasks.into() + ActiveRadarrBlock::SystemTasks.into() ); } @@ -734,10 +706,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into()); SystemDetailsHandler::with( - &SUBMIT_KEY, + SUBMIT_KEY, &mut app, - &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, - &None, + ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + None, ) .handle(); @@ -745,7 +717,7 @@ mod tests { assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemTasks.into() + ActiveRadarrBlock::SystemTasks.into() ); } } @@ -776,10 +748,9 @@ mod tests { .log_details .set_items(vec![HorizontallyScrollableText::default()]); - SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemLogs, &None) - .handle(); + SystemDetailsHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::SystemLogs, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); assert!(app.data.radarr_data.log_details.items.is_empty()); } @@ -795,10 +766,9 @@ mod tests { .tasks .set_items(vec![RadarrTask::default()]); - SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemTasks, &None) - .handle(); + SystemDetailsHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::SystemTasks, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); } #[rstest] @@ -814,14 +784,14 @@ mod tests { .set_items(vec![QueueEvent::default()]); SystemDetailsHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::SystemQueuedEvents, - &None, + ActiveRadarrBlock::SystemQueuedEvents, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); } #[rstest] @@ -831,10 +801,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::System.into()); app.push_navigation_stack(ActiveRadarrBlock::SystemUpdates.into()); - SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemUpdates, &None) + SystemDetailsHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::SystemUpdates, None) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); } #[test] @@ -845,16 +815,16 @@ mod tests { app.data.radarr_data.prompt_confirm = true; SystemDetailsHandler::with( - &ESC_KEY, + ESC_KEY, &mut app, - &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, - &None, + ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemTasks.into() + ActiveRadarrBlock::SystemTasks.into() ); assert!(!app.data.radarr_data.prompt_confirm); } @@ -882,14 +852,14 @@ mod tests { app.push_navigation_stack(active_radarr_block.into()); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); - assert_eq!(app.get_current_route(), &active_radarr_block.into()); + assert_eq!(app.get_current_route(), active_radarr_block.into()); assert!(app.should_refresh); } @@ -909,14 +879,14 @@ mod tests { app.push_navigation_stack(active_radarr_block.into()); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &active_radarr_block, - &None, + active_radarr_block, + None, ) .handle(); - assert_eq!(app.get_current_route(), &active_radarr_block.into()); + assert_eq!(app.get_current_route(), active_radarr_block.into()); assert!(!app.should_refresh); } @@ -928,10 +898,10 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::SystemTaskStartConfirmPrompt.into()); SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.confirm.key, + DEFAULT_KEYBINDINGS.confirm.key, &mut app, - &ActiveRadarrBlock::SystemTaskStartConfirmPrompt, - &None, + ActiveRadarrBlock::SystemTaskStartConfirmPrompt, + None, ) .handle(); @@ -942,7 +912,7 @@ mod tests { ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemTasks.into() + ActiveRadarrBlock::SystemTasks.into() ); } } @@ -951,9 +921,9 @@ mod tests { fn test_system_details_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if SYSTEM_DETAILS_BLOCKS.contains(&active_radarr_block) { - assert!(SystemDetailsHandler::accepts(&active_radarr_block)); + assert!(SystemDetailsHandler::accepts(active_radarr_block)); } else { - assert!(!SystemDetailsHandler::accepts(&active_radarr_block)); + assert!(!SystemDetailsHandler::accepts(active_radarr_block)); } }) } @@ -964,25 +934,25 @@ mod tests { app.is_loading = true; let handler = SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ); assert!(!handler.is_ready()); } #[test] - fn test_system_details_handler_not_ready_when_both_log_details_and_updates_are_empty() { + fn test_system_details_handler_not_ready_when_log_details_and_updates_and_tasks_are_empty() { let mut app = App::default(); app.is_loading = false; let handler = SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ); assert!(!handler.is_ready()); @@ -999,10 +969,30 @@ mod tests { .set_items(vec![HorizontallyScrollableText::default()]); let handler = SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_tasks_is_not_empty() { + let mut app = App::default(); + app.is_loading = false; + app + .data + .radarr_data + .tasks + .set_items(vec![RadarrTask::default()]); + + let handler = SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveRadarrBlock::SystemTasks, + None, ); assert!(handler.is_ready()); @@ -1015,10 +1005,10 @@ mod tests { app.data.radarr_data.updates = ScrollableText::with_string("Test".to_owned()); let handler = SystemDetailsHandler::with( - &DEFAULT_KEYBINDINGS.esc.key, + DEFAULT_KEYBINDINGS.esc.key, &mut app, - &ActiveRadarrBlock::SystemUpdates, - &None, + ActiveRadarrBlock::SystemUpdates, + None, ); assert!(handler.is_ready()); diff --git a/src/handlers/radarr_handlers/system/system_handler_tests.rs b/src/handlers/radarr_handlers/system/system_handler_tests.rs index 03b3aec..46d3cba 100644 --- a/src/handlers/radarr_handlers/system/system_handler_tests.rs +++ b/src/handlers/radarr_handlers/system/system_handler_tests.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod tests { - use pretty_assertions::assert_eq; use rstest::rstest; use strum::IntoEnumIterator; @@ -28,18 +27,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(6); SystemHandler::with( - &DEFAULT_KEYBINDINGS.left.key, + DEFAULT_KEYBINDINGS.left.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Indexers.into() + ActiveRadarrBlock::Indexers.into() ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Indexers.into()); } #[rstest] @@ -49,18 +48,18 @@ mod tests { app.data.radarr_data.main_tabs.set_index(6); SystemHandler::with( - &DEFAULT_KEYBINDINGS.right.key, + DEFAULT_KEYBINDINGS.right.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Movies.into() + ActiveRadarrBlock::Movies.into() ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); } } @@ -79,9 +78,9 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::System.into()); app.push_navigation_stack(ActiveRadarrBlock::System.into()); - SystemHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::System, &None).handle(); + SystemHandler::with(ESC_KEY, &mut app, ActiveRadarrBlock::System, None).handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); assert!(app.error.text.is_empty()); } } @@ -112,16 +111,16 @@ mod tests { .set_items(vec![RadarrTask::default()]); SystemHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemUpdates.into() + ActiveRadarrBlock::SystemUpdates.into() ); } @@ -146,14 +145,14 @@ mod tests { .set_items(vec![RadarrTask::default()]); SystemHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); } #[test] @@ -175,16 +174,16 @@ mod tests { .set_items(vec![RadarrTask::default()]); SystemHandler::with( - &DEFAULT_KEYBINDINGS.events.key, + DEFAULT_KEYBINDINGS.events.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemQueuedEvents.into() + ActiveRadarrBlock::SystemQueuedEvents.into() ); } @@ -209,14 +208,14 @@ mod tests { .set_items(vec![RadarrTask::default()]); SystemHandler::with( - &DEFAULT_KEYBINDINGS.events.key, + DEFAULT_KEYBINDINGS.events.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); } #[test] @@ -239,14 +238,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::System.into()); SystemHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); assert!(app.should_refresh); } @@ -272,14 +271,14 @@ mod tests { app.push_navigation_stack(ActiveRadarrBlock::System.into()); SystemHandler::with( - &DEFAULT_KEYBINDINGS.refresh.key, + DEFAULT_KEYBINDINGS.refresh.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); assert!(!app.should_refresh); } @@ -302,16 +301,16 @@ mod tests { .set_items(vec![RadarrTask::default()]); SystemHandler::with( - &DEFAULT_KEYBINDINGS.logs.key, + DEFAULT_KEYBINDINGS.logs.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemLogs.into() + ActiveRadarrBlock::SystemLogs.into() ); assert_eq!( app.data.radarr_data.log_details.items, @@ -344,14 +343,14 @@ mod tests { .set_items(vec![RadarrTask::default()]); SystemHandler::with( - &DEFAULT_KEYBINDINGS.logs.key, + DEFAULT_KEYBINDINGS.logs.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); assert!(app.data.radarr_data.log_details.is_empty()); } @@ -374,16 +373,16 @@ mod tests { .set_items(vec![RadarrTask::default()]); SystemHandler::with( - &DEFAULT_KEYBINDINGS.tasks.key, + DEFAULT_KEYBINDINGS.tasks.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::SystemTasks.into() + ActiveRadarrBlock::SystemTasks.into() ); } @@ -408,14 +407,14 @@ mod tests { .set_items(vec![RadarrTask::default()]); SystemHandler::with( - &DEFAULT_KEYBINDINGS.tasks.key, + DEFAULT_KEYBINDINGS.tasks.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ) .handle(); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::System.into()); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::System.into()); } } @@ -444,9 +443,9 @@ mod tests { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if system_blocks.contains(&active_radarr_block) { - assert!(SystemHandler::accepts(&active_radarr_block)); + assert!(SystemHandler::accepts(active_radarr_block)); } else { - assert!(!SystemHandler::accepts(&active_radarr_block)); + assert!(!SystemHandler::accepts(active_radarr_block)); } }) } @@ -457,10 +456,10 @@ mod tests { app.is_loading = true; let system_handler = SystemHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ); assert!(!system_handler.is_ready()); @@ -482,10 +481,10 @@ mod tests { .set_items(vec![QueueEvent::default()]); let system_handler = SystemHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ); assert!(!system_handler.is_ready()); @@ -503,10 +502,10 @@ mod tests { .set_items(vec![QueueEvent::default()]); let system_handler = SystemHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ); assert!(!system_handler.is_ready()); @@ -524,10 +523,10 @@ mod tests { .set_items(vec![RadarrTask::default()]); let system_handler = SystemHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ); assert!(!system_handler.is_ready()); @@ -550,10 +549,10 @@ mod tests { .set_items(vec![QueueEvent::default()]); let system_handler = SystemHandler::with( - &DEFAULT_KEYBINDINGS.update.key, + DEFAULT_KEYBINDINGS.update.key, &mut app, - &ActiveRadarrBlock::System, - &None, + ActiveRadarrBlock::System, + None, ); assert!(system_handler.is_ready()); diff --git a/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs new file mode 100644 index 0000000..f8bcd9a --- /dev/null +++ b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs @@ -0,0 +1,621 @@ +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use chrono::DateTime; + use pretty_assertions::{assert_eq, assert_str_eq}; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::blocklist::{blocklist_sorting_options, BlocklistHandler}; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS}; + use crate::models::servarr_models::{Language, Quality, QualityWrapper}; + use crate::models::sonarr_models::BlocklistItem; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_blocklist_item_prompt() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::Blocklist, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::DeleteBlocklistItemPrompt.into() + ); + } + + #[test] + fn test_delete_blocklist_item_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::Blocklist, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_blocklist_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(2); + + BlocklistHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Downloads.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Downloads.into()); + } + + #[rstest] + fn test_blocklist_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(2); + + BlocklistHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::History.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[rstest] + fn test_blocklist_left_right_prompt_toggle( + #[values( + ActiveSonarrBlock::DeleteBlocklistItemPrompt, + ActiveSonarrBlock::BlocklistClearAllItemsPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + + BlocklistHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + BlocklistHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_blocklist_submit() { + let mut app = App::default(); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + + BlocklistHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::Blocklist, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::BlocklistItemDetails.into() + ); + } + + #[test] + fn test_blocklist_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + + BlocklistHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::Blocklist, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + } + + #[rstest] + #[case( + ActiveSonarrBlock::Blocklist, + ActiveSonarrBlock::DeleteBlocklistItemPrompt, + SonarrEvent::DeleteBlocklistItem(None) + )] + #[case( + ActiveSonarrBlock::Blocklist, + ActiveSonarrBlock::BlocklistClearAllItemsPrompt, + SonarrEvent::ClearBlocklist + )] + fn test_blocklist_prompt_confirm_submit( + #[case] base_route: ActiveSonarrBlock, + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + ) { + let mut app = App::default(); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + assert_eq!(app.get_current_route(), base_route.into()); + } + + #[rstest] + fn test_blocklist_prompt_decline_submit( + #[values( + ActiveSonarrBlock::DeleteBlocklistItemPrompt, + ActiveSonarrBlock::BlocklistClearAllItemsPrompt + )] + prompt_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + #[case( + ActiveSonarrBlock::Blocklist, + ActiveSonarrBlock::DeleteBlocklistItemPrompt + )] + #[case( + ActiveSonarrBlock::Blocklist, + ActiveSonarrBlock::BlocklistClearAllItemsPrompt + )] + fn test_blocklist_prompt_blocks_esc( + #[case] base_block: ActiveSonarrBlock, + #[case] prompt_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(base_block.into()); + app.push_navigation_stack(prompt_block.into()); + app.data.sonarr_data.prompt_confirm = true; + + BlocklistHandler::with(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert_eq!(app.get_current_route(), base_block.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[test] + fn test_esc_blocklist_item_details() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveSonarrBlock::BlocklistItemDetails.into()); + + BlocklistHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::BlocklistItemDetails, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + + BlocklistHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::Blocklist, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + #[test] + fn test_refresh_blocklist_key() { + let mut app = App::default(); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + + BlocklistHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_blocklist_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + + BlocklistHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_clear_blocklist_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::with( + DEFAULT_KEYBINDINGS.clear.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::BlocklistClearAllItemsPrompt.into() + ); + } + + #[test] + fn test_clear_blocklist_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + + BlocklistHandler::with( + DEFAULT_KEYBINDINGS.clear.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + } + + #[rstest] + #[case( + ActiveSonarrBlock::Blocklist, + ActiveSonarrBlock::DeleteBlocklistItemPrompt, + SonarrEvent::DeleteBlocklistItem(None) + )] + #[case( + ActiveSonarrBlock::Blocklist, + ActiveSonarrBlock::BlocklistClearAllItemsPrompt, + SonarrEvent::ClearBlocklist + )] + fn test_blocklist_prompt_confirm( + #[case] base_route: ActiveSonarrBlock, + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + ) { + let mut app = App::default(); + app.data.sonarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + assert_eq!(app.get_current_route(), base_route.into()); + } + } + + #[test] + fn test_blocklist_sorting_options_series_title() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.series_title + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp( + &b.series_title + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase(), + ) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[0].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Series Title"); + } + + #[test] + fn test_blocklist_sorting_options_source_title() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[1].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Source Title"); + } + + #[test] + fn test_blocklist_sorting_options_language() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + let a_languages = a + .languages + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + let b_languages = b + .languages + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + + a_languages.cmp(&b_languages) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[2].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Language"); + } + + #[test] + fn test_blocklist_sorting_options_quality() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[3].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_blocklist_sorting_options_date() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = + |a, b| a.date.cmp(&b.date); + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[4].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Date"); + } + + #[test] + fn test_blocklist_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if BLOCKLIST_BLOCKS.contains(&active_sonarr_block) { + assert!(BlocklistHandler::accepts(active_sonarr_block)); + } else { + assert!(!BlocklistHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_blocklist_handler_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.is_loading = true; + + let handler = BlocklistHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_blocklist_handler_not_ready_when_blocklist_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.is_loading = false; + + let handler = BlocklistHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_blocklist_handler_ready_when_not_loading_and_blocklist_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); + app.is_loading = false; + app + .data + .sonarr_data + .blocklist + .set_items(vec![BlocklistItem::default()]); + + let handler = BlocklistHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Blocklist, + None, + ); + + assert!(handler.is_ready()); + } + + fn blocklist_vec() -> Vec { + vec![ + BlocklistItem { + id: 3, + source_title: "test 1".to_owned(), + languages: vec![Language { + id: 1, + name: "telgu".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()), + series_title: Some("test 3".into()), + ..BlocklistItem::default() + }, + BlocklistItem { + id: 2, + source_title: "test 2".to_owned(), + languages: vec![Language { + id: 3, + name: "chinese".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "SD - 720p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + series_title: Some("test 2".into()), + ..BlocklistItem::default() + }, + BlocklistItem { + id: 1, + source_title: "test 3".to_owned(), + languages: vec![Language { + id: 1, + name: "english".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()), + series_title: None, + ..BlocklistItem::default() + }, + ] + } +} diff --git a/src/handlers/sonarr_handlers/blocklist/mod.rs b/src/handlers/sonarr_handlers/blocklist/mod.rs new file mode 100644 index 0000000..9843558 --- /dev/null +++ b/src/handlers/sonarr_handlers/blocklist/mod.rs @@ -0,0 +1,232 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS}; +use crate::models::sonarr_models::BlocklistItem; +use crate::models::stateful_table::SortOption; +use crate::network::sonarr_network::SonarrEvent; + +#[cfg(test)] +#[path = "blocklist_handler_tests.rs"] +mod blocklist_handler_tests; + +pub(super) struct BlocklistHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> BlocklistHandler<'a, 'b> { + handle_table_events!( + self, + blocklist, + self.app.data.sonarr_data.blocklist, + BlocklistItem + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for BlocklistHandler<'a, 'b> { + fn handle(&mut self) { + let blocklist_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::Blocklist.into()) + .sorting_block(ActiveSonarrBlock::BlocklistSortPrompt.into()) + .sort_by_fn(|a: &BlocklistItem, b: &BlocklistItem| a.id.cmp(&b.id)) + .sort_options(blocklist_sorting_options()); + + if !self.handle_blocklist_table_events(blocklist_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + BLOCKLIST_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> Self { + BlocklistHandler { + key, + app, + active_sonarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.sonarr_data.blocklist.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::Blocklist { + self + .app + .push_navigation_stack(ActiveSonarrBlock::DeleteBlocklistItemPrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::Blocklist => handle_change_tab_left_right_keys(self.app, self.key), + ActiveSonarrBlock::DeleteBlocklistItemPrompt + | ActiveSonarrBlock::BlocklistClearAllItemsPrompt => handle_prompt_toggle(self.app, self.key), + _ => {} + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::DeleteBlocklistItemPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteBlocklistItem(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::BlocklistClearAllItemsPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::ClearBlocklist); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::Blocklist => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::BlocklistItemDetails.into()); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::DeleteBlocklistItemPrompt + | ActiveSonarrBlock::BlocklistClearAllItemsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + ActiveSonarrBlock::BlocklistItemDetails | ActiveSonarrBlock::BlocklistSortPrompt => { + self.app.pop_navigation_stack(); + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::Blocklist => match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ if key == DEFAULT_KEYBINDINGS.clear.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::BlocklistClearAllItemsPrompt.into()); + } + _ => (), + }, + ActiveSonarrBlock::DeleteBlocklistItemPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteBlocklistItem(None)); + + self.app.pop_navigation_stack(); + } + } + ActiveSonarrBlock::BlocklistClearAllItemsPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::ClearBlocklist); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } +} + +fn blocklist_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Series Title", + cmp_fn: Some(|a, b| { + a.series_title + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp( + &b.series_title + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase(), + ) + }), + }, + SortOption { + name: "Source Title", + cmp_fn: Some(|a, b| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }), + }, + SortOption { + name: "Language", + cmp_fn: Some(|a, b| { + let a_languages = a + .languages + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + let b_languages = b + .languages + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + + a_languages.cmp(&b_languages) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }), + }, + SortOption { + name: "Date", + cmp_fn: Some(|a, b| a.date.cmp(&b.date)), + }, + ] +} diff --git a/src/handlers/sonarr_handlers/downloads/downloads_handler_tests.rs b/src/handlers/sonarr_handlers/downloads/downloads_handler_tests.rs new file mode 100644 index 0000000..dd2ac0f --- /dev/null +++ b/src/handlers/sonarr_handlers/downloads/downloads_handler_tests.rs @@ -0,0 +1,442 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::downloads::DownloadsHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS}; + use crate::models::sonarr_models::DownloadRecord; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_download_prompt() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::Downloads, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::DeleteDownloadPrompt.into() + ); + } + + #[test] + fn test_delete_download_prompt_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::Downloads, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Downloads.into()); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_downloads_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(1); + + DownloadsHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Series.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + + #[rstest] + fn test_downloads_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(1); + + DownloadsHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Blocklist.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_downloads_left_right_prompt_toggle( + #[values( + ActiveSonarrBlock::DeleteDownloadPrompt, + ActiveSonarrBlock::UpdateDownloadsPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + + DownloadsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + DownloadsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[rstest] + #[case( + ActiveSonarrBlock::Downloads, + ActiveSonarrBlock::DeleteDownloadPrompt, + SonarrEvent::DeleteDownload(None) + )] + #[case( + ActiveSonarrBlock::Downloads, + ActiveSonarrBlock::UpdateDownloadsPrompt, + SonarrEvent::UpdateDownloads + )] + fn test_downloads_prompt_confirm_submit( + #[case] base_route: ActiveSonarrBlock, + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + ) { + let mut app = App::default(); + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + DownloadsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + assert_eq!(app.get_current_route(), base_route.into()); + } + + #[rstest] + #[case(ActiveSonarrBlock::Downloads, ActiveSonarrBlock::DeleteDownloadPrompt)] + #[case(ActiveSonarrBlock::Downloads, ActiveSonarrBlock::UpdateDownloadsPrompt)] + fn test_downloads_prompt_decline_submit( + #[case] base_route: ActiveSonarrBlock, + #[case] prompt_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + DownloadsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert_eq!(app.get_current_route(), base_route.into()); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + #[case(ActiveSonarrBlock::Downloads, ActiveSonarrBlock::DeleteDownloadPrompt)] + #[case(ActiveSonarrBlock::Downloads, ActiveSonarrBlock::UpdateDownloadsPrompt)] + fn test_downloads_prompt_blocks_esc( + #[case] base_block: ActiveSonarrBlock, + #[case] prompt_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(base_block.into()); + app.push_navigation_stack(prompt_block.into()); + app.data.sonarr_data.prompt_confirm = true; + + DownloadsHandler::with(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert_eq!(app.get_current_route(), base_block.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + + DownloadsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::Downloads, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Downloads.into()); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + #[test] + fn test_update_downloads_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::UpdateDownloadsPrompt.into() + ); + } + + #[test] + fn test_update_downloads_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Downloads.into()); + } + + #[test] + fn test_refresh_downloads_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + + DownloadsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Downloads.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_downloads_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Downloads.into()); + assert!(!app.should_refresh); + } + + #[rstest] + #[case( + ActiveSonarrBlock::Downloads, + ActiveSonarrBlock::DeleteDownloadPrompt, + SonarrEvent::DeleteDownload(None) + )] + #[case( + ActiveSonarrBlock::Downloads, + ActiveSonarrBlock::UpdateDownloadsPrompt, + SonarrEvent::UpdateDownloads + )] + fn test_downloads_prompt_confirm_submit( + #[case] base_route: ActiveSonarrBlock, + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + ) { + let mut app = App::default(); + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + DownloadsHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + assert_eq!(app.get_current_route(), base_route.into()); + } + } + + #[test] + fn test_downloads_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if DOWNLOADS_BLOCKS.contains(&active_sonarr_block) { + assert!(DownloadsHandler::accepts(active_sonarr_block)); + } else { + assert!(!DownloadsHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_downloads_handler_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app.is_loading = true; + + let handler = DownloadsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_downloads_handler_not_ready_when_downloads_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app.is_loading = false; + + let handler = DownloadsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_downloads_handler_ready_when_not_loading_and_downloads_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Downloads.into()); + app.is_loading = false; + + app + .data + .sonarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + let handler = DownloadsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Downloads, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/downloads/mod.rs b/src/handlers/sonarr_handlers/downloads/mod.rs new file mode 100644 index 0000000..6b1fe51 --- /dev/null +++ b/src/handlers/sonarr_handlers/downloads/mod.rs @@ -0,0 +1,157 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS}; +use crate::models::sonarr_models::DownloadRecord; +use crate::network::sonarr_network::SonarrEvent; + +#[cfg(test)] +#[path = "downloads_handler_tests.rs"] +mod downloads_handler_tests; + +pub(super) struct DownloadsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> DownloadsHandler<'a, 'b> { + handle_table_events!( + self, + downloads, + self.app.data.sonarr_data.downloads, + DownloadRecord + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DownloadsHandler<'a, 'b> { + fn handle(&mut self) { + let download_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::Downloads.into()); + + if !self.handle_downloads_table_events(download_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + DOWNLOADS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> DownloadsHandler<'a, 'b> { + DownloadsHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.sonarr_data.downloads.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::Downloads { + self + .app + .push_navigation_stack(ActiveSonarrBlock::DeleteDownloadPrompt.into()) + } + } + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::Downloads => handle_change_tab_left_right_keys(self.app, self.key), + ActiveSonarrBlock::DeleteDownloadPrompt | ActiveSonarrBlock::UpdateDownloadsPrompt => { + handle_prompt_toggle(self.app, self.key) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::DeleteDownloadPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteDownload(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::UpdateDownloadsPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::UpdateDownloads); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::DeleteDownloadPrompt | ActiveSonarrBlock::UpdateDownloadsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::Downloads => match self.key { + _ if key == DEFAULT_KEYBINDINGS.update.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::UpdateDownloadsPrompt.into()); + } + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ => (), + }, + ActiveSonarrBlock::DeleteDownloadPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteDownload(None)); + + self.app.pop_navigation_stack(); + } + } + ActiveSonarrBlock::UpdateDownloadsPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::UpdateDownloads); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/history/history_handler_tests.rs b/src/handlers/sonarr_handlers/history/history_handler_tests.rs new file mode 100644 index 0000000..c932f20 --- /dev/null +++ b/src/handlers/sonarr_handlers/history/history_handler_tests.rs @@ -0,0 +1,414 @@ +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use chrono::DateTime; + use pretty_assertions::{assert_eq, assert_str_eq}; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::history::{history_sorting_options, HistoryHandler}; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; + use crate::models::servarr_models::{Language, Quality, QualityWrapper}; + use crate::models::sonarr_models::{SonarrHistoryEventType, SonarrHistoryItem}; + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_history_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(3); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Blocklist.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_history_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(3); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_history_submit() { + let mut app = App::default(); + app.data.sonarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + + HistoryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::History, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::HistoryItemDetails.into() + ); + } + + #[test] + fn test_history_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + + HistoryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_esc_history_item_details() { + let mut app = App::default(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::HistoryItemDetails.into()); + + HistoryHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::HistoryItemDetails, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + + HistoryHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_refresh_history_key() { + let mut app = App::default(); + app.data.sonarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_history_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(!app.should_refresh); + } + } + + #[test] + fn test_history_sorting_options_source_title() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = |a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[0].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Source Title"); + } + + #[test] + fn test_history_sorting_options_event_type() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = |a, b| { + a.event_type + .to_string() + .to_lowercase() + .cmp(&b.event_type.to_string().to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[1].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Event Type"); + } + + #[test] + fn test_history_sorting_options_language() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = |a, b| { + let default_language = Language { + id: 1, + name: "_".to_owned(), + }; + let language_a = &a.languages.first().unwrap_or(&default_language); + let language_b = &b.languages.first().unwrap_or(&default_language); + + language_a.cmp(language_b) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[2].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Language"); + } + + #[test] + fn test_history_sorting_options_quality() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = |a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[3].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_history_sorting_options_date() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = + |a, b| a.date.cmp(&b.date); + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[4].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Date"); + } + + #[test] + fn test_history_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if HISTORY_BLOCKS.contains(&active_sonarr_block) { + assert!(HistoryHandler::accepts(active_sonarr_block)); + } else { + assert!(!HistoryHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_history_handler_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = true; + + let handler = HistoryHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_not_ready_when_history_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = false; + + let handler = HistoryHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_ready_when_not_loading_and_history_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = false; + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + + let handler = HistoryHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::History, + None, + ); + + assert!(handler.is_ready()); + } + + fn history_vec() -> Vec { + vec![ + SonarrHistoryItem { + id: 3, + source_title: "test 1".into(), + event_type: SonarrHistoryEventType::Grabbed, + languages: vec![Language { + id: 1, + name: "telgu".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()), + ..SonarrHistoryItem::default() + }, + SonarrHistoryItem { + id: 2, + source_title: "test 2".into(), + event_type: SonarrHistoryEventType::DownloadFolderImported, + languages: vec![Language { + id: 3, + name: "chinese".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "SD - 720p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + ..SonarrHistoryItem::default() + }, + SonarrHistoryItem { + id: 1, + source_title: "test 3".into(), + event_type: SonarrHistoryEventType::EpisodeFileDeleted, + languages: vec![Language { + id: 1, + name: "english".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()), + ..SonarrHistoryItem::default() + }, + ] + } +} diff --git a/src/handlers/sonarr_handlers/history/mod.rs b/src/handlers/sonarr_handlers/history/mod.rs new file mode 100644 index 0000000..2ad58a6 --- /dev/null +++ b/src/handlers/sonarr_handlers/history/mod.rs @@ -0,0 +1,171 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::{handle_clear_errors, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; +use crate::models::servarr_models::Language; +use crate::models::sonarr_models::SonarrHistoryItem; +use crate::models::stateful_table::SortOption; + +#[cfg(test)] +#[path = "history_handler_tests.rs"] +mod history_handler_tests; + +pub(super) struct HistoryHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> HistoryHandler<'a, 'b> { + handle_table_events!( + self, + history, + self.app.data.sonarr_data.history, + SonarrHistoryItem + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, 'b> { + fn handle(&mut self) { + let history_table_handling_config = TableHandlingConfig::new(ActiveSonarrBlock::History.into()) + .sorting_block(ActiveSonarrBlock::HistorySortPrompt.into()) + .sort_by_fn(|a: &SonarrHistoryItem, b: &SonarrHistoryItem| a.id.cmp(&b.id)) + .sort_options(history_sorting_options()) + .searching_block(ActiveSonarrBlock::SearchHistory.into()) + .search_error_block(ActiveSonarrBlock::SearchHistoryError.into()) + .search_field_fn(|history| &history.source_title.text) + .filtering_block(ActiveSonarrBlock::FilterHistory.into()) + .filter_error_block(ActiveSonarrBlock::FilterHistoryError.into()) + .filter_field_fn(|history| &history.source_title.text); + + if !self.handle_history_table_events(history_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + HISTORY_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> Self { + HistoryHandler { + key, + app, + active_sonarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.sonarr_data.history.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::History { + handle_change_tab_left_right_keys(self.app, self.key) + } + } + + fn handle_submit(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::History { + self + .app + .push_navigation_stack(ActiveSonarrBlock::HistoryItemDetails.into()); + } + } + + fn handle_esc(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::HistoryItemDetails { + self.app.pop_navigation_stack(); + } else { + handle_clear_errors(self.app); + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + if self.active_sonarr_block == ActiveSonarrBlock::History { + match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ => (), + } + } + } +} + +pub(in crate::handlers::sonarr_handlers) fn history_sorting_options( +) -> Vec> { + vec![ + SortOption { + name: "Source Title", + cmp_fn: Some(|a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }), + }, + SortOption { + name: "Event Type", + cmp_fn: Some(|a, b| { + a.event_type + .to_string() + .to_lowercase() + .cmp(&b.event_type.to_string().to_lowercase()) + }), + }, + SortOption { + name: "Language", + cmp_fn: Some(|a, b| { + let default_language = Language { + id: 1, + name: "_".to_owned(), + }; + let language_a = &a.languages.first().unwrap_or(&default_language); + let language_b = &b.languages.first().unwrap_or(&default_language); + + language_a.cmp(language_b) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }), + }, + SortOption { + name: "Date", + cmp_fn: Some(|a, b| a.date.cmp(&b.date)), + }, + ] +} diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs new file mode 100644 index 0000000..ca41e51 --- /dev/null +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler.rs @@ -0,0 +1,476 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; +use crate::network::sonarr_network::SonarrEvent; +use crate::{handle_prompt_left_right_keys, handle_text_box_keys, handle_text_box_left_right_keys}; + +#[cfg(test)] +#[path = "edit_indexer_handler_tests.rs"] +mod edit_indexer_handler_tests; + +pub(super) struct EditIndexerHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditIndexerHandler<'a, 'b> { + fn accepts(active_block: ActiveSonarrBlock) -> bool { + EDIT_INDEXER_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> EditIndexerHandler<'a, 'b> { + EditIndexerHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.sonarr_data.edit_indexer_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditIndexerPrompt => { + self.app.data.sonarr_data.selected_block.up(); + } + ActiveSonarrBlock::EditIndexerPriorityInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .priority += 1; + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditIndexerPrompt => { + self.app.data.sonarr_data.selected_block.down(); + } + ActiveSonarrBlock::EditIndexerPriorityInput => { + let edit_indexer_modal = self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + if edit_indexer_modal.priority > 0 { + edit_indexer_modal.priority -= 1; + } + } + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditIndexerNameInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + .scroll_home(); + } + ActiveSonarrBlock::EditIndexerUrlInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + .scroll_home(); + } + ActiveSonarrBlock::EditIndexerApiKeyInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + .scroll_home(); + } + ActiveSonarrBlock::EditIndexerSeedRatioInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + .scroll_home(); + } + ActiveSonarrBlock::EditIndexerTagsInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + .scroll_home(); + } + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditIndexerNameInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + .reset_offset(); + } + ActiveSonarrBlock::EditIndexerUrlInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + .reset_offset(); + } + ActiveSonarrBlock::EditIndexerApiKeyInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + .reset_offset(); + } + ActiveSonarrBlock::EditIndexerSeedRatioInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + .reset_offset(); + } + ActiveSonarrBlock::EditIndexerTagsInput => { + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + .reset_offset(); + } + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditIndexerPrompt => { + handle_prompt_left_right_keys!( + self, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + sonarr_data + ); + } + ActiveSonarrBlock::EditIndexerNameInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + ); + } + ActiveSonarrBlock::EditIndexerUrlInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + ); + } + ActiveSonarrBlock::EditIndexerApiKeyInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + ); + } + ActiveSonarrBlock::EditIndexerSeedRatioInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + ); + } + ActiveSonarrBlock::EditIndexerTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + ); + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditIndexerPrompt => { + let selected_block = self.app.data.sonarr_data.selected_block.get_active_block(); + match selected_block { + ActiveSonarrBlock::EditIndexerConfirmPrompt => { + let sonarr_data = &mut self.app.data.sonarr_data; + if sonarr_data.prompt_confirm { + sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditIndexer(None)); + self.app.should_refresh = true; + } else { + sonarr_data.edit_indexer_modal = None; + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::EditIndexerNameInput + | ActiveSonarrBlock::EditIndexerUrlInput + | ActiveSonarrBlock::EditIndexerApiKeyInput + | ActiveSonarrBlock::EditIndexerSeedRatioInput + | ActiveSonarrBlock::EditIndexerTagsInput => { + self.app.push_navigation_stack(selected_block.into()); + self.app.should_ignore_quit_key = true; + } + ActiveSonarrBlock::EditIndexerPriorityInput => self + .app + .push_navigation_stack(ActiveSonarrBlock::EditIndexerPriorityInput.into()), + ActiveSonarrBlock::EditIndexerToggleEnableRss => { + let indexer = self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_rss = Some(!indexer.enable_rss.unwrap_or_default()); + } + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch => { + let indexer = self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_automatic_search = + Some(!indexer.enable_automatic_search.unwrap_or_default()); + } + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch => { + let indexer = self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap(); + indexer.enable_interactive_search = + Some(!indexer.enable_interactive_search.unwrap_or_default()); + } + _ => (), + } + } + ActiveSonarrBlock::EditIndexerNameInput + | ActiveSonarrBlock::EditIndexerUrlInput + | ActiveSonarrBlock::EditIndexerApiKeyInput + | ActiveSonarrBlock::EditIndexerSeedRatioInput + | ActiveSonarrBlock::EditIndexerTagsInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::EditIndexerPriorityInput => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditIndexerPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + self.app.data.sonarr_data.edit_indexer_modal = None; + } + ActiveSonarrBlock::EditIndexerNameInput + | ActiveSonarrBlock::EditIndexerUrlInput + | ActiveSonarrBlock::EditIndexerApiKeyInput + | ActiveSonarrBlock::EditIndexerSeedRatioInput + | ActiveSonarrBlock::EditIndexerPriorityInput + | ActiveSonarrBlock::EditIndexerTagsInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + _ => self.app.pop_navigation_stack(), + } + } + + fn handle_char_key_event(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditIndexerNameInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .name + ); + } + ActiveSonarrBlock::EditIndexerUrlInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .url + ); + } + ActiveSonarrBlock::EditIndexerApiKeyInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .api_key + ); + } + ActiveSonarrBlock::EditIndexerSeedRatioInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .seed_ratio + ); + } + ActiveSonarrBlock::EditIndexerTagsInput => { + handle_text_box_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_indexer_modal + .as_mut() + .unwrap() + .tags + ); + } + ActiveSonarrBlock::EditIndexerPrompt => { + if self.app.data.sonarr_data.selected_block.get_active_block() + == ActiveSonarrBlock::EditIndexerConfirmPrompt + && self.key == DEFAULT_KEYBINDINGS.confirm.key + { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditIndexer(None)); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs new file mode 100644 index 0000000..1008ec2 --- /dev/null +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -0,0 +1,1791 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; + use strum::IntoEnumIterator; + + mod test_handle_scroll_up_and_down { + use crate::app::App; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_data::sonarr::sonarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; + use crate::models::BlockSelectionState; + + use super::*; + + #[rstest] + fn test_edit_indexer_priority_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + key, + &mut app, + ActiveSonarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 1 + ); + } else { + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 0 + ); + + EditIndexerHandler::with( + Key::Up, + &mut app, + ActiveSonarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 1 + ); + + EditIndexerHandler::with( + key, + &mut app, + ActiveSonarrBlock::EditIndexerPriorityInput, + None, + ) + .handle(); + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .priority, + 0 + ); + } + } + + #[rstest] + fn test_edit_indexer_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditIndexerNameInput + ); + } else { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch + ); + } + } + + #[rstest] + fn test_edit_indexer_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = true; + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditIndexerToggleEnableRss + ); + } + } + + mod test_handle_home_end { + use std::sync::atomic::Ordering; + + use crate::app::App; + use crate::models::servarr_data::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_edit_indexer_name_input_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_url_input_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_api_key_input_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_tags_input_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_left_right_action { + use std::sync::atomic::Ordering; + + use crate::app::App; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_data::sonarr::sonarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::models::BlockSelectionState; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.y = EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1; + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[rstest] + #[case( + 0, + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveSonarrBlock::EditIndexerToggleEnableRss, + ActiveSonarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveSonarrBlock::EditIndexerSeedRatioInput + )] + #[case( + 3, + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveSonarrBlock::EditIndexerTagsInput + )] + fn test_left_right_block_toggle_torrents( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_y_index: usize, + #[case] left_block: ActiveSonarrBlock, + #[case] right_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.y = starting_y_index; + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + left_block + ); + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + right_block + ); + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + left_block + ); + } + + #[rstest] + #[case( + 0, + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveSonarrBlock::EditIndexerToggleEnableRss, + ActiveSonarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveSonarrBlock::EditIndexerTagsInput + )] + #[case( + 3, + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveSonarrBlock::EditIndexerPriorityInput + )] + fn test_left_right_block_toggle_nzb( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_y_index: usize, + #[case] left_block: ActiveSonarrBlock, + #[case] right_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.y = starting_y_index; + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + left_block + ); + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + right_block + ); + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + left_block + ); + } + + #[rstest] + fn test_left_right_block_toggle_torren_empty_row_to_prompt_confirm( + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.y = 4; + app.data.sonarr_data.prompt_confirm = false; + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditIndexerPriorityInput + ); + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditIndexerConfirmPrompt + ); + + EditIndexerHandler::with(key, &mut app, ActiveSonarrBlock::EditIndexerPrompt, None).handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditIndexerConfirmPrompt + ); + assert!(app.data.sonarr_data.prompt_confirm); + } + + #[test] + fn test_edit_indexer_name_input_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_url_input_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_api_key_input_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_indexer_tags_input_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::app::App; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::{ + servarr_data::sonarr::sonarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, BlockSelectionState, + }; + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_indexer_prompt_prompt_decline_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert!(!app.should_refresh); + assert_eq!(app.data.sonarr_data.edit_indexer_modal, None); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.sonarr_data.prompt_confirm = true; + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(app.data.sonarr_data.edit_indexer_modal.is_some()); + assert!(app.should_refresh); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::EditIndexer(None)) + ); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.sonarr_data.prompt_confirm = true; + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + assert!(app.data.sonarr_data.edit_indexer_modal.is_some()); + assert!(!app.should_refresh); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + } + + #[rstest] + #[case(0, 0, ActiveSonarrBlock::EditIndexerNameInput)] + #[case(0, 1, ActiveSonarrBlock::EditIndexerUrlInput)] + #[case(1, 1, ActiveSonarrBlock::EditIndexerApiKeyInput)] + #[case(2, 1, ActiveSonarrBlock::EditIndexerSeedRatioInput)] + #[case(3, 1, ActiveSonarrBlock::EditIndexerTagsInput)] + fn test_edit_indexer_prompt_submit_input_fields( + #[case] starting_y: usize, + #[case] starting_x: usize, + #[case] block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(starting_x, starting_y); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), block.into()); + assert!(app.should_ignore_quit_key); + } + + #[test] + fn test_edit_indexer_priority_input_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, 4); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPriorityInput.into() + ); + assert!(!app.should_ignore_quit_key); + } + + #[test] + fn test_edit_indexer_toggle_enable_rss_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, 1); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + assert!(app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_rss + .unwrap()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + assert!(!app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_rss + .unwrap()); + } + + #[test] + fn test_edit_indexer_toggle_enable_automatic_search_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, 2); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + assert!(app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_automatic_search + .unwrap()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + assert!(!app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_automatic_search + .unwrap()); + } + + #[test] + fn test_edit_indexer_toggle_enable_interactive_search_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, 3); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + assert!(app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_interactive_search + .unwrap()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + assert!(!app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_interactive_search + .unwrap()); + } + + #[test] + fn test_edit_indexer_name_input_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.should_ignore_quit_key = true; + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerNameInput.into()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + } + + #[test] + fn test_edit_indexer_url_input_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.should_ignore_quit_key = true; + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerUrlInput.into()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + } + + #[test] + fn test_edit_indexer_api_key_input_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.should_ignore_quit_key = true; + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerApiKeyInput.into()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.should_ignore_quit_key = true; + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerSeedRatioInput.into()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + } + + #[test] + fn test_edit_indexer_tags_input_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.should_ignore_quit_key = true; + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerTagsInput.into()); + + EditIndexerHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + } + } + + mod test_handle_esc { + use super::*; + use crate::app::App; + use crate::event::Key; + use crate::models::servarr_data::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_edit_indexer_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.data.sonarr_data.edit_indexer_modal, None); + } + + #[rstest] + fn test_edit_indexer_input_fields_esc( + #[values( + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerUrlInput, + ActiveSonarrBlock::EditIndexerApiKeyInput, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + ActiveSonarrBlock::EditIndexerTagsInput, + ActiveSonarrBlock::EditIndexerPriorityInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(active_sonarr_block.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.should_ignore_quit_key = true; + + EditIndexerHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.edit_indexer_modal, + Some(EditIndexerModal::default()) + ); + } + } + + mod test_handle_key_char { + use crate::app::App; + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_data::sonarr::sonarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; + use crate::models::BlockSelectionState; + use crate::network::sonarr_network::SonarrEvent; + use pretty_assertions::assert_str_eq; + + use super::*; + + #[test] + fn test_edit_indexer_name_input_backspace() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_url_input_backspace() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_api_key_input_backspace() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_backspace() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_tags_input_backspace() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_name_input_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::EditIndexerNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text, + "h" + ); + } + + #[test] + fn test_edit_indexer_url_input_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::EditIndexerUrlInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text, + "h" + ); + } + + #[test] + fn test_edit_indexer_api_key_input_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::EditIndexerApiKeyInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text, + "h" + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text, + "h" + ); + } + + #[test] + fn test_edit_indexer_tags_input_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::EditIndexerTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text, + "h" + ); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_confirm() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(app.data.sonarr_data.edit_indexer_modal.is_some()); + assert!(app.should_refresh); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::EditIndexer(None)) + ); + } + } + + #[test] + fn test_edit_indexer_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if EDIT_INDEXER_BLOCKS.contains(&active_sonarr_block) { + assert!(EditIndexerHandler::accepts(active_sonarr_block)); + } else { + assert!(!EditIndexerHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_edit_indexer_handler_is_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_handler_is_not_ready_when_edit_indexer_modal_is_none() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_handler_is_ready_when_edit_indexer_modal_is_some() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = false; + app.data.sonarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + let handler = EditIndexerHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EditIndexerPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs new file mode 100644 index 0000000..94afa1d --- /dev/null +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -0,0 +1,182 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handle_prompt_left_right_keys; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, +}; +use crate::network::sonarr_network::SonarrEvent; + +#[cfg(test)] +#[path = "edit_indexer_settings_handler_tests.rs"] +mod edit_indexer_settings_handler_tests; + +pub(super) struct IndexerSettingsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexerSettingsHandler<'a, 'b> { + fn accepts(active_block: ActiveSonarrBlock) -> bool { + INDEXER_SETTINGS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> IndexerSettingsHandler<'a, 'b> { + IndexerSettingsHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.sonarr_data.indexer_settings.is_some() + } + + fn handle_scroll_up(&mut self) { + let indexer_settings = self.app.data.sonarr_data.indexer_settings.as_mut().unwrap(); + match self.active_sonarr_block { + ActiveSonarrBlock::AllIndexerSettingsPrompt => { + self.app.data.sonarr_data.selected_block.up(); + } + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput => { + indexer_settings.minimum_age += 1; + } + ActiveSonarrBlock::IndexerSettingsRetentionInput => { + indexer_settings.retention += 1; + } + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput => { + indexer_settings.maximum_size += 1; + } + ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput => { + indexer_settings.rss_sync_interval += 1; + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + let indexer_settings = self.app.data.sonarr_data.indexer_settings.as_mut().unwrap(); + match self.active_sonarr_block { + ActiveSonarrBlock::AllIndexerSettingsPrompt => { + self.app.data.sonarr_data.selected_block.down() + } + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput => { + if indexer_settings.minimum_age > 0 { + indexer_settings.minimum_age -= 1; + } + } + ActiveSonarrBlock::IndexerSettingsRetentionInput => { + if indexer_settings.retention > 0 { + indexer_settings.retention -= 1; + } + } + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput => { + if indexer_settings.maximum_size > 0 { + indexer_settings.maximum_size -= 1; + } + } + ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput => { + if indexer_settings.rss_sync_interval > 0 { + indexer_settings.rss_sync_interval -= 1; + } + } + _ => (), + } + } + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::AllIndexerSettingsPrompt { + handle_prompt_left_right_keys!( + self, + ActiveSonarrBlock::IndexerSettingsConfirmPrompt, + sonarr_data + ); + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AllIndexerSettingsPrompt => { + match self.app.data.sonarr_data.selected_block.get_active_block() { + ActiveSonarrBlock::IndexerSettingsConfirmPrompt => { + let sonarr_data = &mut self.app.data.sonarr_data; + if sonarr_data.prompt_confirm { + sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditAllIndexerSettings(None)); + self.app.should_refresh = true; + } else { + sonarr_data.indexer_settings = None; + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput + | ActiveSonarrBlock::IndexerSettingsRetentionInput + | ActiveSonarrBlock::IndexerSettingsMaximumSizeInput + | ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput => { + self.app.push_navigation_stack( + ( + self.app.data.sonarr_data.selected_block.get_active_block(), + None, + ) + .into(), + ) + } + + _ => (), + } + } + + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput + | ActiveSonarrBlock::IndexerSettingsRetentionInput + | ActiveSonarrBlock::IndexerSettingsMaximumSizeInput + | ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AllIndexerSettingsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + self.app.data.sonarr_data.indexer_settings = None; + } + _ => self.app.pop_navigation_stack(), + } + } + + fn handle_char_key_event(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::AllIndexerSettingsPrompt + && self.app.data.sonarr_data.selected_block.get_active_block() + == ActiveSonarrBlock::IndexerSettingsConfirmPrompt + && self.key == DEFAULT_KEYBINDINGS.confirm.key + { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::EditAllIndexerSettings(None)); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } +} diff --git a/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs new file mode 100644 index 0000000..8301125 --- /dev/null +++ b/src/handlers/sonarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -0,0 +1,570 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, + }; + use crate::models::sonarr_models::IndexerSettings; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS; + use crate::models::sonarr_models::IndexerSettings; + use crate::models::BlockSelectionState; + + use super::*; + + macro_rules! test_i64_counter_scroll_value { + ($block:expr, $key:expr, $data_ref:ident, $negatives:literal) => { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::with($key, &mut app, $block, None).handle(); + + if $key == Key::Up { + assert_eq!( + app + .data + .sonarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 1 + ); + } else { + if $negatives { + assert_eq!( + app + .data + .sonarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + -1 + ); + } else { + assert_eq!( + app + .data + .sonarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 0 + ); + + IndexerSettingsHandler::with(Key::Up, &mut app, $block, None).handle(); + + assert_eq!( + app + .data + .sonarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 1 + ); + + IndexerSettingsHandler::with($key, &mut app, $block, None).handle(); + assert_eq!( + app + .data + .sonarr_data + .indexer_settings + .as_ref() + .unwrap() + .$data_ref, + 0 + ); + } + } + }; + } + + #[rstest] + fn test_edit_indexer_settings_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + IndexerSettingsHandler::with( + key, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput + ); + } else { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput + ); + } + } + + #[rstest] + fn test_edit_indexer_settings_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = true; + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + IndexerSettingsHandler::with( + key, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::IndexerSettingsRetentionInput + ); + } + + #[rstest] + fn test_edit_indexer_settings_minimum_age_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput, + key, + minimum_age, + false + ); + } + + #[rstest] + fn test_edit_indexer_settings_retention_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveSonarrBlock::IndexerSettingsRetentionInput, + key, + retention, + false + ); + } + + #[rstest] + fn test_edit_indexer_settings_maximum_size_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput, + key, + maximum_size, + false + ); + } + + #[rstest] + fn test_edit_indexer_settings_rss_sync_interval_scroll(#[values(Key::Up, Key::Down)] key: Key) { + test_i64_counter_scroll_value!( + ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput, + key, + rss_sync_interval, + false + ); + } + } + + mod test_handle_left_right_action { + use crate::models::servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS; + + use crate::models::BlockSelectionState; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.y = INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1; + + IndexerSettingsHandler::with( + key, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + IndexerSettingsHandler::with( + key, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::{ + models::{ + servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + sonarr_models::IndexerSettings, BlockSelectionState, + }, + network::sonarr_network::SonarrEvent, + }; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_indexer_settings_prompt_prompt_decline_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert!(!app.should_refresh); + assert_eq!(app.data.sonarr_data.indexer_settings, None); + } + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.sonarr_data.prompt_confirm = true; + + IndexerSettingsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::EditAllIndexerSettings(None)) + ); + assert!(app.data.sonarr_data.indexer_settings.is_some()); + assert!(app.should_refresh); + } + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + app.data.sonarr_data.prompt_confirm = true; + + IndexerSettingsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AllIndexerSettingsPrompt.into() + ); + assert!(!app.should_refresh); + } + + #[rstest] + #[case(ActiveSonarrBlock::IndexerSettingsMinimumAgeInput, 0)] + #[case(ActiveSonarrBlock::IndexerSettingsRetentionInput, 1)] + #[case(ActiveSonarrBlock::IndexerSettingsMaximumSizeInput, 2)] + #[case(ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput, 3)] + fn test_edit_indexer_settings_prompt_submit_selected_block( + #[case] selected_block: ActiveSonarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, y_index); + + IndexerSettingsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), selected_block.into()); + } + + #[rstest] + fn test_edit_indexer_settings_prompt_submit_selected_block_no_op_when_not_ready( + #[values(0, 1, 2, 3, 4)] y_index: usize, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = true; + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, y_index); + + IndexerSettingsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AllIndexerSettingsPrompt.into() + ); + } + + #[rstest] + fn test_edit_indexer_settings_selected_block_submit( + #[values( + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput, + ActiveSonarrBlock::IndexerSettingsRetentionInput, + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput, + ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + app.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); + app.push_navigation_stack(active_sonarr_block.into()); + + IndexerSettingsHandler::with(SUBMIT_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AllIndexerSettingsPrompt.into() + ); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::sonarr_models::IndexerSettings; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_edit_indexer_settings_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.data.sonarr_data.indexer_settings, None); + } + + #[rstest] + fn test_edit_indexer_settings_selected_blocks_esc( + #[values( + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput, + ActiveSonarrBlock::IndexerSettingsRetentionInput, + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput, + ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(active_sonarr_block.into()); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert_eq!( + app.data.sonarr_data.indexer_settings, + Some(IndexerSettings::default()) + ); + } + } + + mod test_handle_key_char { + use crate::{ + models::{ + servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + sonarr_models::IndexerSettings, BlockSelectionState, + }, + network::sonarr_network::SonarrEvent, + }; + + use super::*; + + #[test] + fn test_edit_indexer_settings_prompt_prompt_confirmation_confirm() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, INDEXER_SETTINGS_SELECTION_BLOCKS.len() - 1); + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + + IndexerSettingsHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::EditAllIndexerSettings(None)) + ); + assert!(app.data.sonarr_data.indexer_settings.is_some()); + assert!(app.should_refresh); + } + } + + #[test] + fn test_indexer_settings_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if INDEXER_SETTINGS_BLOCKS.contains(&active_sonarr_block) { + assert!(IndexerSettingsHandler::accepts(active_sonarr_block)); + } else { + assert!(!IndexerSettingsHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_edit_indexer_settings_handler_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = IndexerSettingsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_settings_handler_not_ready_when_indexer_settings_is_none() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = IndexerSettingsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_indexer_settings_handler_ready_when_not_loading_and_indexer_settings_is_some() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = false; + app.data.sonarr_data.indexer_settings = Some(IndexerSettings::default()); + + let handler = IndexerSettingsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/indexers_handler_tests.rs new file mode 100644 index 0000000..9a4173d --- /dev/null +++ b/src/handlers/sonarr_handlers/indexers/indexers_handler_tests.rs @@ -0,0 +1,704 @@ +#[cfg(test)] +mod tests { + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::indexers::IndexersHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, + }; + use crate::models::servarr_models::Indexer; + use crate::test_handler_delegation; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_indexer_prompt() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::Indexers, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::DeleteIndexerPrompt.into() + ); + } + + #[test] + fn test_delete_indexer_prompt_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::Indexers, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_indexers_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(5); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + + #[rstest] + fn test_indexers_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(5); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::System.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + } + + #[rstest] + fn test_left_right_delete_indexer_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + + IndexersHandler::with(key, &mut app, ActiveSonarrBlock::DeleteIndexerPrompt, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + IndexersHandler::with(key, &mut app, ActiveSonarrBlock::DeleteIndexerPrompt, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use crate::models::servarr_data::modals::EditIndexerModal; + use crate::models::servarr_data::sonarr::sonarr_data::{ + SonarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::models::servarr_models::{Indexer, IndexerField}; + use bimap::BiMap; + use pretty_assertions::assert_eq; + use serde_json::{Number, Value}; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[rstest] + fn test_edit_indexer_submit(#[values(true, false)] torrent_protocol: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + let protocol = if torrent_protocol { + "torrent".to_owned() + } else { + "usenet".to_owned() + }; + let mut expected_edit_indexer_modal = EditIndexerModal { + name: "Test".into(), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: "https://test.com".into(), + api_key: "1234".into(), + tags: "usenet, test".into(), + ..EditIndexerModal::default() + }; + let mut sonarr_data = SonarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..SonarrData::default() + }; + let mut fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + ]; + + if torrent_protocol { + fields.push(IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(Value::from(1.2f64)), + }); + expected_edit_indexer_modal.seed_ratio = "1.2".into(); + } + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + protocol, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + ..Indexer::default() + }; + sonarr_data.indexers.set_items(vec![indexer]); + app.data.sonarr_data = sonarr_data; + + IndexersHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::Indexers, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditIndexerPrompt.into() + ); + assert_eq!( + app.data.sonarr_data.edit_indexer_modal, + Some((&app.data.sonarr_data).into()) + ); + assert_eq!( + app.data.sonarr_data.edit_indexer_modal, + Some(expected_edit_indexer_modal) + ); + if torrent_protocol { + assert_eq!( + app.data.sonarr_data.selected_block.blocks, + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS + ); + } else { + assert_eq!( + app.data.sonarr_data.selected_block.blocks, + EDIT_INDEXER_NZB_SELECTION_BLOCKS + ); + } + } + + #[test] + fn test_edit_indexer_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::Indexers, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert_eq!(app.data.sonarr_data.edit_indexer_modal, None); + } + + #[test] + fn test_delete_indexer_prompt_confirm_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteIndexer(None)) + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + + #[test] + fn test_prompt_decline_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_delete_indexer_prompt_block_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteIndexerPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + + IndexersHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[rstest] + fn test_test_indexer_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.data.sonarr_data.indexer_test_errors = Some("test result".to_owned()); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::TestIndexer.into()); + + IndexersHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::TestIndexer, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert_eq!(app.data.sonarr_data.indexer_test_errors, None); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + + IndexersHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::Indexers, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use crate::{ + models::servarr_data::sonarr::sonarr_data::INDEXER_SETTINGS_SELECTION_BLOCKS, + network::sonarr_network::SonarrEvent, + }; + + use super::*; + + #[test] + fn test_refresh_indexers_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_indexers_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_indexer_settings_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.settings.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AllIndexerSettingsPrompt.into() + ); + assert_eq!( + app.data.sonarr_data.selected_block.blocks, + INDEXER_SETTINGS_SELECTION_BLOCKS + ); + } + + #[test] + fn test_indexer_settings_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.settings.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + + #[test] + fn test_test_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.test.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::TestIndexer.into() + ); + } + + #[test] + fn test_test_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.test.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + + #[test] + fn test_test_all_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.test_all.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::TestAllIndexers.into() + ); + } + + #[test] + fn test_test_all_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.test_all.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + + #[test] + fn test_delete_indexer_prompt_confirm() { + let mut app = App::default(); + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteIndexerPrompt.into()); + + IndexersHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::DeleteIndexerPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteIndexer(None)) + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + } + + #[rstest] + fn test_delegates_edit_indexer_blocks_to_edit_indexer_handler( + #[values( + ActiveSonarrBlock::EditIndexerPrompt, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ActiveSonarrBlock::EditIndexerApiKeyInput, + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + ActiveSonarrBlock::EditIndexerToggleEnableRss, + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveSonarrBlock::EditIndexerUrlInput, + ActiveSonarrBlock::EditIndexerTagsInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + IndexersHandler, + ActiveSonarrBlock::Indexers, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler( + #[values( + ActiveSonarrBlock::AllIndexerSettingsPrompt, + ActiveSonarrBlock::IndexerSettingsConfirmPrompt, + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput, + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput, + ActiveSonarrBlock::IndexerSettingsRetentionInput, + ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + IndexersHandler, + ActiveSonarrBlock::Indexers, + active_sonarr_block + ); + } + + #[test] + fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() { + test_handler_delegation!( + IndexersHandler, + ActiveSonarrBlock::Indexers, + ActiveSonarrBlock::TestAllIndexers + ); + } + + #[test] + fn test_indexers_handler_accepts() { + let mut indexers_blocks = Vec::new(); + indexers_blocks.extend(INDEXERS_BLOCKS); + indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS); + indexers_blocks.extend(EDIT_INDEXER_BLOCKS); + indexers_blocks.push(ActiveSonarrBlock::TestAllIndexers); + + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if indexers_blocks.contains(&active_sonarr_block) { + assert!(IndexersHandler::accepts(active_sonarr_block)); + } else { + assert!(!IndexersHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_indexers_handler_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = IndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_indexers_handler_not_ready_when_indexers_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = IndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_indexers_handler_ready_when_not_loading_and_indexers_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = false; + app + .data + .sonarr_data + .indexers + .set_items(vec![Indexer::default()]); + + let handler = IndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Indexers, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/indexers/mod.rs b/src/handlers/sonarr_handlers/indexers/mod.rs new file mode 100644 index 0000000..72e6908 --- /dev/null +++ b/src/handlers/sonarr_handlers/indexers/mod.rs @@ -0,0 +1,200 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::sonarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; +use crate::handlers::sonarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; +use crate::handlers::sonarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, +}; +use crate::models::servarr_models::Indexer; +use crate::models::BlockSelectionState; +use crate::network::sonarr_network::SonarrEvent; + +mod edit_indexer_handler; +mod edit_indexer_settings_handler; +mod test_all_indexers_handler; + +#[cfg(test)] +#[path = "indexers_handler_tests.rs"] +mod indexers_handler_tests; + +pub(super) struct IndexersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + context: Option, +} + +impl<'a, 'b> IndexersHandler<'a, 'b> { + handle_table_events!(self, indexers, self.app.data.sonarr_data.indexers, Indexer); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for IndexersHandler<'a, 'b> { + fn handle(&mut self) { + let indexers_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::Indexers.into()); + + if !self.handle_indexers_table_events(indexers_table_handling_config) { + match self.active_sonarr_block { + _ if EditIndexerHandler::accepts(self.active_sonarr_block) => { + EditIndexerHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle() + } + _ if IndexerSettingsHandler::accepts(self.active_sonarr_block) => { + IndexerSettingsHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle() + } + _ if TestAllIndexersHandler::accepts(self.active_sonarr_block) => { + TestAllIndexersHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle() + } + _ => self.handle_key_event(), + } + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + EditIndexerHandler::accepts(active_block) + || IndexerSettingsHandler::accepts(active_block) + || TestAllIndexersHandler::accepts(active_block) + || INDEXERS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> IndexersHandler<'a, 'b> { + IndexersHandler { + key, + app, + active_sonarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.sonarr_data.indexers.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::Indexers { + self + .app + .push_navigation_stack(ActiveSonarrBlock::DeleteIndexerPrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::Indexers => handle_change_tab_left_right_keys(self.app, self.key), + ActiveSonarrBlock::DeleteIndexerPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::DeleteIndexerPrompt => { + let sonarr_data = &mut self.app.data.sonarr_data; + if sonarr_data.prompt_confirm { + sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteIndexer(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::Indexers => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::EditIndexerPrompt.into()); + self.app.data.sonarr_data.edit_indexer_modal = Some((&self.app.data.sonarr_data).into()); + let protocol = &self + .app + .data + .sonarr_data + .indexers + .current_selection() + .protocol; + if protocol == "torrent" { + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + } else { + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_INDEXER_NZB_SELECTION_BLOCKS); + } + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::DeleteIndexerPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + ActiveSonarrBlock::TestIndexer => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.indexer_test_errors = None; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::Indexers => match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ if key == DEFAULT_KEYBINDINGS.test.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::TestIndexer.into()); + } + _ if key == DEFAULT_KEYBINDINGS.test_all.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::TestAllIndexers.into()); + } + _ if key == DEFAULT_KEYBINDINGS.settings.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AllIndexerSettingsPrompt.into()); + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(INDEXER_SETTINGS_SELECTION_BLOCKS); + } + _ => (), + }, + ActiveSonarrBlock::DeleteIndexerPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteIndexer(None)); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs b/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs new file mode 100644 index 0000000..91ff186 --- /dev/null +++ b/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler.rs @@ -0,0 +1,101 @@ +use crate::app::App; +use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::KeyEventHandler; +use crate::models::servarr_data::modals::IndexerTestResultModalItem; +use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; + +#[cfg(test)] +#[path = "test_all_indexers_handler_tests.rs"] +mod test_all_indexers_handler_tests; + +pub(super) struct TestAllIndexersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> TestAllIndexersHandler<'a, 'b> { + handle_table_events!( + self, + indexer_test_all_results, + self + .app + .data + .sonarr_data + .indexer_test_all_results + .as_mut() + .unwrap(), + IndexerTestResultModalItem + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for TestAllIndexersHandler<'a, 'b> { + fn handle(&mut self) { + let indexer_test_all_results_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::TestAllIndexers.into()); + + if !self + .handle_indexer_test_all_results_table_events(indexer_test_all_results_table_handling_config) + { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + active_block == ActiveSonarrBlock::TestAllIndexers + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> TestAllIndexersHandler<'a, 'b> { + TestAllIndexersHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + let table_is_ready = if let Some(table) = &self.app.data.sonarr_data.indexer_test_all_results { + !table.is_empty() + } else { + false + }; + + !self.app.is_loading && table_is_ready + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) {} + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::TestAllIndexers { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.indexer_test_all_results = None; + } + } + + fn handle_char_key_event(&mut self) {} +} diff --git a/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler_tests.rs b/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler_tests.rs new file mode 100644 index 0000000..be828d1 --- /dev/null +++ b/src/handlers/sonarr_handlers/indexers/test_all_indexers_handler_tests.rs @@ -0,0 +1,118 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::handlers::sonarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::modals::IndexerTestResultModalItem; + use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; + use crate::models::stateful_table::StatefulTable; + use strum::IntoEnumIterator; + + mod test_handle_esc { + use super::*; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_test_all_indexers_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveSonarrBlock::TestAllIndexers.into()); + app.data.sonarr_data.indexer_test_all_results = Some(StatefulTable::default()); + + TestAllIndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::TestAllIndexers, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.indexer_test_all_results.is_none()); + } + } + + #[test] + fn test_test_all_indexers_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if active_sonarr_block == ActiveSonarrBlock::TestAllIndexers { + assert!(TestAllIndexersHandler::accepts(active_sonarr_block)); + } else { + assert!(!TestAllIndexersHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_test_all_indexers_handler_is_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = true; + + let handler = TestAllIndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::TestAllIndexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_test_all_indexers_handler_is_not_ready_when_results_is_none() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = false; + + let handler = TestAllIndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::TestAllIndexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_test_all_indexers_handler_is_not_ready_when_results_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = false; + app.data.sonarr_data.indexer_test_all_results = Some(StatefulTable::default()); + + let handler = TestAllIndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::TestAllIndexers, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_test_all_indexers_handler_is_ready_when_results_is_not_empty_and_is_loaded() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Indexers.into()); + app.is_loading = false; + let mut indexer_test_results = StatefulTable::default(); + indexer_test_results.set_items(vec![IndexerTestResultModalItem::default()]); + app.data.sonarr_data.indexer_test_all_results = Some(indexer_test_results); + + let handler = TestAllIndexersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::TestAllIndexers, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/library/add_series_handler.rs b/src/handlers/sonarr_handlers/library/add_series_handler.rs new file mode 100644 index 0000000..80d3c35 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/add_series_handler.rs @@ -0,0 +1,544 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, +}; +use crate::models::sonarr_models::AddSeriesSearchResult; +use crate::models::{BlockSelectionState, Scrollable}; +use crate::network::sonarr_network::SonarrEvent; +use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys, App, Key}; + +#[cfg(test)] +#[path = "add_series_handler_tests.rs"] +mod add_series_handler_tests; + +pub(super) struct AddSeriesHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> AddSeriesHandler<'a, 'b> { + handle_table_events!( + self, + add_searched_series, + self + .app + .data + .sonarr_data + .add_searched_series + .as_mut() + .unwrap(), + AddSeriesSearchResult + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for AddSeriesHandler<'a, 'b> { + fn handle(&mut self) { + let add_series_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::AddSeriesSearchResults.into()); + + if !self.handle_add_searched_series_table_events(add_series_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + ADD_SERIES_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> AddSeriesHandler<'a, 'b> { + AddSeriesHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + } + + fn handle_scroll_up(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AddSeriesSelectMonitor => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_up(), + ActiveSonarrBlock::AddSeriesSelectSeriesType => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .series_type_list + .scroll_up(), + ActiveSonarrBlock::AddSeriesSelectQualityProfile => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_up(), + ActiveSonarrBlock::AddSeriesSelectLanguageProfile => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .language_profile_list + .scroll_up(), + ActiveSonarrBlock::AddSeriesSelectRootFolder => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_up(), + ActiveSonarrBlock::AddSeriesPrompt => self.app.data.sonarr_data.selected_block.up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AddSeriesSelectMonitor => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_down(), + ActiveSonarrBlock::AddSeriesSelectSeriesType => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .series_type_list + .scroll_down(), + ActiveSonarrBlock::AddSeriesSelectQualityProfile => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_down(), + ActiveSonarrBlock::AddSeriesSelectLanguageProfile => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .language_profile_list + .scroll_down(), + ActiveSonarrBlock::AddSeriesSelectRootFolder => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_down(), + ActiveSonarrBlock::AddSeriesPrompt => self.app.data.sonarr_data.selected_block.down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AddSeriesSelectMonitor => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_top(), + ActiveSonarrBlock::AddSeriesSelectSeriesType => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .series_type_list + .scroll_to_top(), + ActiveSonarrBlock::AddSeriesSelectQualityProfile => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_top(), + ActiveSonarrBlock::AddSeriesSelectLanguageProfile => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .language_profile_list + .scroll_to_top(), + ActiveSonarrBlock::AddSeriesSelectRootFolder => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_to_top(), + ActiveSonarrBlock::AddSeriesSearchInput => self + .app + .data + .sonarr_data + .add_series_search + .as_mut() + .unwrap() + .scroll_home(), + ActiveSonarrBlock::AddSeriesTagsInput => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .tags + .scroll_home(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AddSeriesSelectMonitor => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_bottom(), + ActiveSonarrBlock::AddSeriesSelectSeriesType => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .series_type_list + .scroll_to_bottom(), + ActiveSonarrBlock::AddSeriesSelectQualityProfile => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_bottom(), + ActiveSonarrBlock::AddSeriesSelectLanguageProfile => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .language_profile_list + .scroll_to_bottom(), + ActiveSonarrBlock::AddSeriesSelectRootFolder => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .root_folder_list + .scroll_to_bottom(), + ActiveSonarrBlock::AddSeriesSearchInput => self + .app + .data + .sonarr_data + .add_series_search + .as_mut() + .unwrap() + .reset_offset(), + ActiveSonarrBlock::AddSeriesTagsInput => self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .tags + .reset_offset(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AddSeriesPrompt => handle_prompt_toggle(self.app, self.key), + ActiveSonarrBlock::AddSeriesSearchInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .add_series_search + .as_mut() + .unwrap() + ) + } + ActiveSonarrBlock::AddSeriesTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .tags + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + _ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchInput + && !self + .app + .data + .sonarr_data + .add_series_search + .as_mut() + .unwrap() + .text + .is_empty() => + { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); + self.app.should_ignore_quit_key = false; + } + _ if self.active_sonarr_block == ActiveSonarrBlock::AddSeriesSearchResults + && self.app.data.sonarr_data.add_searched_series.is_some() => + { + let tvdb_id = self + .app + .data + .sonarr_data + .add_searched_series + .as_ref() + .unwrap() + .current_selection() + .tvdb_id; + + if self + .app + .data + .sonarr_data + .series + .items + .iter() + .any(|series| series.tvdb_id == tvdb_id) + { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AddSeriesAlreadyInLibrary.into()); + } else { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + self.app.data.sonarr_data.add_series_modal = Some((&self.app.data.sonarr_data).into()); + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); + } + } + ActiveSonarrBlock::AddSeriesPrompt => { + match self.app.data.sonarr_data.selected_block.get_active_block() { + ActiveSonarrBlock::AddSeriesConfirmPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddSeries(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::AddSeriesSelectMonitor + | ActiveSonarrBlock::AddSeriesSelectSeriesType + | ActiveSonarrBlock::AddSeriesSelectQualityProfile + | ActiveSonarrBlock::AddSeriesSelectLanguageProfile + | ActiveSonarrBlock::AddSeriesSelectRootFolder => self.app.push_navigation_stack( + self + .app + .data + .sonarr_data + .selected_block + .get_active_block() + .into(), + ), + ActiveSonarrBlock::AddSeriesTagsInput => { + self.app.push_navigation_stack( + self + .app + .data + .sonarr_data + .selected_block + .get_active_block() + .into(), + ); + self.app.should_ignore_quit_key = true; + } + ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder => { + self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .use_season_folder = !self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .use_season_folder; + } + _ => (), + } + } + ActiveSonarrBlock::AddSeriesSelectMonitor + | ActiveSonarrBlock::AddSeriesSelectSeriesType + | ActiveSonarrBlock::AddSeriesSelectQualityProfile + | ActiveSonarrBlock::AddSeriesSelectLanguageProfile + | ActiveSonarrBlock::AddSeriesSelectRootFolder => self.app.pop_navigation_stack(), + ActiveSonarrBlock::AddSeriesTagsInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AddSeriesSearchInput => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.add_series_search = None; + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::AddSeriesSearchResults + | ActiveSonarrBlock::AddSeriesEmptySearchResults => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.add_searched_series = None; + self.app.should_ignore_quit_key = true; + } + ActiveSonarrBlock::AddSeriesPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.add_series_modal = None; + self.app.data.sonarr_data.prompt_confirm = false; + } + ActiveSonarrBlock::AddSeriesSelectMonitor + | ActiveSonarrBlock::AddSeriesSelectSeriesType + | ActiveSonarrBlock::AddSeriesSelectQualityProfile + | ActiveSonarrBlock::AddSeriesSelectLanguageProfile + | ActiveSonarrBlock::AddSeriesAlreadyInLibrary + | ActiveSonarrBlock::AddSeriesSelectRootFolder => self.app.pop_navigation_stack(), + ActiveSonarrBlock::AddSeriesTagsInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::AddSeriesSearchInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .sonarr_data + .add_series_search + .as_mut() + .unwrap() + ) + } + ActiveSonarrBlock::AddSeriesTagsInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .tags + ) + } + ActiveSonarrBlock::AddSeriesPrompt => { + if self.app.data.sonarr_data.selected_block.get_active_block() + == ActiveSonarrBlock::AddSeriesConfirmPrompt + && key == DEFAULT_KEYBINDINGS.confirm.key + { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddSeries(None)); + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs b/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs new file mode 100644 index 0000000..d00fdf1 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/add_series_handler_tests.rs @@ -0,0 +1,1625 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_str_eq; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::library::add_series_handler::AddSeriesHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS}; + use crate::models::servarr_models::RootFolder; + use crate::models::sonarr_models::{AddSeriesSearchResult, SeriesMonitor, SeriesType}; + use crate::models::HorizontallyScrollableText; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::modals::AddSeriesModal; + use crate::models::servarr_data::sonarr::sonarr_data::ADD_SERIES_SELECTION_BLOCKS; + use crate::models::BlockSelectionState; + use crate::simple_stateful_iterable_vec; + + use super::*; + + #[rstest] + fn test_add_series_select_monitor_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_vec = Vec::from_iter(SeriesMonitor::iter()); + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_vec.len()).rev() { + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[i] + ); + } + } else { + for i in 0..monitor_vec.len() { + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[(i + 1) % monitor_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_series_select_series_type_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let series_type_vec = Vec::from_iter(SeriesType::iter()); + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .series_type_list + .set_items(series_type_vec.clone()); + + if key == Key::Up { + for i in (0..series_type_vec.len()).rev() { + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .series_type_list + .current_selection(), + &series_type_vec[i] + ); + } + } else { + for i in 0..series_type_vec.len() { + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .series_type_list + .current_selection(), + &series_type_vec[(i + 1) % series_type_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_series_select_quality_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 2" + ); + + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_add_series_select_language_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .language_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .language_profile_list + .current_selection(), + "Test 2" + ); + + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .language_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_add_series_select_root_folder_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .root_folder_list + .set_items(simple_stateful_iterable_vec!(RootFolder, String, path)); + + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 2" + ); + + AddSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 1" + ); + } + + #[rstest] + fn test_add_series_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + AddSeriesHandler::with(key, &mut app, ActiveSonarrBlock::AddSeriesPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::AddSeriesSelectRootFolder + ); + } else { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::AddSeriesSelectQualityProfile + ); + } + } + + #[rstest] + fn test_add_series_prompt_scroll_no_op_when_not_ready(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = true; + app.data.sonarr_data.selected_block = BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + AddSeriesHandler::with(key, &mut app, ActiveSonarrBlock::AddSeriesPrompt, None).handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::AddSeriesSelectMonitor + ); + } + } + + mod test_handle_home_end { + use std::sync::atomic::Ordering; + + use strum::IntoEnumIterator; + + use crate::extended_stateful_iterable_vec; + use crate::models::servarr_data::sonarr::modals::AddSeriesModal; + + use super::*; + + #[test] + fn test_add_series_select_monitor_home_end() { + let monitor_vec = Vec::from_iter(SeriesMonitor::iter()); + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_vec.clone()); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[monitor_vec.len() - 1] + ); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_vec[0] + ); + } + + #[test] + fn test_add_series_select_series_type_home_end() { + let series_type_vec = Vec::from_iter(SeriesType::iter()); + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .series_type_list + .set_items(series_type_vec.clone()); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .series_type_list + .current_selection(), + &series_type_vec[series_type_vec.len() - 1] + ); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .series_type_list + .current_selection(), + &series_type_vec[0] + ); + } + + #[test] + fn test_add_series_select_quality_profile_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 3" + ); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_add_series_select_language_profile_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .language_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .language_profile_list + .current_selection(), + "Test 3" + ); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .language_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_add_series_select_root_folder_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .root_folder_list + .set_items(extended_stateful_iterable_vec!(RootFolder, String, path)); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 3" + ); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::AddSeriesSelectRootFolder, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .root_folder_list + .current_selection() + .path, + "Test 1" + ); + } + + #[test] + fn test_add_series_search_input_home_end_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_search = Some("Test".into()); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::AddSeriesSearchInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_search + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 4 + ); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::AddSeriesSearchInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_search + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_series_tags_input_home_end_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal { + tags: "Test".into(), + ..AddSeriesModal::default() + }); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::AddSeriesTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::AddSeriesTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_left_right_action { + use std::sync::atomic::Ordering; + + use crate::models::servarr_data::sonarr::modals::AddSeriesModal; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + AddSeriesHandler::with(key, &mut app, ActiveSonarrBlock::AddSeriesPrompt, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + AddSeriesHandler::with(key, &mut app, ActiveSonarrBlock::AddSeriesPrompt, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[test] + fn test_add_series_search_input_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_search = Some("Test".into()); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::AddSeriesSearchInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_search + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::AddSeriesSearchInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_search + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_series_tags_input_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal { + tags: "Test".into(), + ..AddSeriesModal::default() + }); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::AddSeriesTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::AddSeriesTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + + use crate::models::servarr_data::sonarr::modals::AddSeriesModal; + use crate::models::servarr_data::sonarr::sonarr_data::ADD_SERIES_SELECTION_BLOCKS; + use crate::models::sonarr_models::Series; + use crate::models::stateful_table::StatefulTable; + use crate::models::BlockSelectionState; + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_add_series_search_input_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.should_ignore_quit_key = true; + app.data.sonarr_data.add_series_search = Some("test".into()); + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesSearchInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesSearchResults.into() + ); + } + + #[test] + fn test_add_series_search_input_submit_noop_on_empty_search() { + let mut app = App::default(); + app.data.sonarr_data.add_series_search = Some(HorizontallyScrollableText::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchInput.into()); + app.should_ignore_quit_key = true; + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesSearchInput, + None, + ) + .handle(); + + assert!(app.should_ignore_quit_key); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesSearchInput.into() + ); + } + + #[test] + fn test_add_series_search_results_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + let mut add_searched_series = StatefulTable::default(); + add_searched_series.set_items(vec![AddSeriesSearchResult::default()]); + app.data.sonarr_data.add_searched_series = Some(add_searched_series); + app.data.sonarr_data.quality_profile_map = + BiMap::from_iter([(1, "B - Test 2".to_owned()), (0, "A - Test 1".to_owned())]); + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesPrompt.into() + ); + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::AddSeriesSelectRootFolder + ); + assert!(app.data.sonarr_data.add_series_modal.is_some()); + assert!(!app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .monitor_list + .items + .is_empty()); + assert!(!app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .series_type_list + .items + .is_empty()); + assert!(!app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .items + .is_empty()); + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "A - Test 1" + ); + } + + #[test] + fn test_add_series_search_results_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); + let mut add_searched_series = StatefulTable::default(); + add_searched_series.set_items(vec![AddSeriesSearchResult::default()]); + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesSearchResults.into() + ); + assert!(app.data.sonarr_data.add_series_modal.is_none()); + } + + #[test] + fn test_add_series_search_results_submit_does_nothing_on_empty_table() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesSearchResults.into() + ); + } + + #[test] + fn test_add_series_search_results_submit_series_already_in_library() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + let mut add_searched_series = StatefulTable::default(); + add_searched_series.set_items(vec![AddSeriesSearchResult::default()]); + app.data.sonarr_data.add_searched_series = Some(add_searched_series); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesSearchResults, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesAlreadyInLibrary.into() + ); + } + + #[test] + fn test_add_series_prompt_prompt_decline_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, ADD_SERIES_SELECTION_BLOCKS.len() - 1); + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + } + + #[test] + fn test_add_series_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::default(); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.selected_block = BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, ADD_SERIES_SELECTION_BLOCKS.len() - 1); + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::AddSeries(None)) + ); + assert!(app.data.sonarr_data.add_series_modal.is_some()); + } + + #[rstest] + #[case(ActiveSonarrBlock::AddSeriesSelectRootFolder, 0)] + #[case(ActiveSonarrBlock::AddSeriesSelectMonitor, 1)] + #[case(ActiveSonarrBlock::AddSeriesSelectQualityProfile, 2)] + #[case(ActiveSonarrBlock::AddSeriesSelectLanguageProfile, 3)] + #[case(ActiveSonarrBlock::AddSeriesSelectSeriesType, 4)] + #[case(ActiveSonarrBlock::AddSeriesTagsInput, 6)] + fn test_add_series_prompt_selected_block_submit( + #[case] selected_block: ActiveSonarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, y_index); + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), selected_block.into()); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + + if selected_block == ActiveSonarrBlock::AddSeriesTagsInput { + assert!(app.should_ignore_quit_key); + } + } + + #[rstest] + fn test_add_series_prompt_selecting_preferences_blocks_submit( + #[values( + ActiveSonarrBlock::AddSeriesSelectMonitor, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + ActiveSonarrBlock::AddSeriesSelectRootFolder, + ActiveSonarrBlock::AddSeriesTagsInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + app.push_navigation_stack(active_sonarr_block.into()); + + AddSeriesHandler::with(SUBMIT_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesPrompt.into() + ); + + if active_sonarr_block == ActiveSonarrBlock::AddSeriesTagsInput { + assert!(!app.should_ignore_quit_key); + } + } + + #[test] + fn test_add_series_toggle_use_season_folder_submit() { + let mut app = App::default(); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, 5); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesPrompt.into() + ); + assert!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .use_season_folder + ); + + AddSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesPrompt.into() + ); + assert!( + !app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .use_season_folder + ); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::sonarr::modals::AddSeriesModal; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::models::stateful_table::StatefulTable; + use crate::simple_stateful_iterable_vec; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_add_series_search_input_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.data.sonarr_data = create_test_sonarr_data(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchInput.into()); + + AddSeriesHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesSearchInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!(app.data.sonarr_data.add_series_search, None); + } + + #[test] + fn test_add_series_input_esc() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesTagsInput.into()); + + AddSeriesHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesTagsInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesPrompt.into() + ); + } + + #[rstest] + fn test_add_series_search_results_esc( + #[values( + ActiveSonarrBlock::AddSeriesSearchResults, + ActiveSonarrBlock::AddSeriesEmptySearchResults + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchInput.into()); + app.push_navigation_stack(active_sonarr_block.into()); + let mut add_searched_series = StatefulTable::default(); + add_searched_series.set_items(simple_stateful_iterable_vec!( + AddSeriesSearchResult, + HorizontallyScrollableText + )); + app.data.sonarr_data.add_searched_series = Some(add_searched_series); + + AddSeriesHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesSearchInput.into() + ); + assert!(app.data.sonarr_data.add_searched_series.is_none()); + assert!(app.should_ignore_quit_key); + } + + #[test] + fn test_add_series_already_in_library_esc() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesAlreadyInLibrary.into()); + + AddSeriesHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesAlreadyInLibrary, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesSearchResults.into() + ); + } + + #[test] + fn test_add_series_prompt_esc() { + let mut app = App::default(); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchResults.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + + AddSeriesHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::AddSeriesPrompt, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesSearchResults.into() + ); + assert!(app.data.sonarr_data.add_series_modal.is_none()); + } + + #[test] + fn test_add_series_tags_input_esc() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesTagsInput.into()); + + AddSeriesHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::AddSeriesTagsInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesPrompt.into() + ); + } + + #[rstest] + fn test_selecting_preferences_blocks_esc( + #[values( + ActiveSonarrBlock::AddSeriesSelectMonitor, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + ActiveSonarrBlock::AddSeriesSelectRootFolder + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + app.push_navigation_stack(active_sonarr_block.into()); + + AddSeriesHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesPrompt.into() + ); + } + } + + mod test_handle_key_char { + use super::*; + use crate::{ + models::{ + servarr_data::sonarr::{modals::AddSeriesModal, sonarr_data::ADD_SERIES_SELECTION_BLOCKS}, + BlockSelectionState, + }, + network::sonarr_network::SonarrEvent, + }; + + #[test] + fn test_add_series_search_input_backspace() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_search = Some("Test".into()); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::AddSeriesSearchInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_search + .as_ref() + .unwrap() + .text, + "Tes" + ); + } + + #[test] + fn test_add_series_tags_input_backspace() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal { + tags: "Test".into(), + ..AddSeriesModal::default() + }); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::AddSeriesTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_add_series_search_input_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_search = Some(HorizontallyScrollableText::default()); + + AddSeriesHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::AddSeriesSearchInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_search + .as_ref() + .unwrap() + .text, + "h" + ); + } + + #[test] + fn test_add_series_tags_input_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + + AddSeriesHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::AddSeriesTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .add_series_modal + .as_ref() + .unwrap() + .tags + .text, + "h" + ); + } + + #[test] + fn test_add_series_confirm_prompt_prompt_confirmation_confirm() { + let mut app = App::default(); + app.data.sonarr_data.add_series_modal = Some(AddSeriesModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddSeriesPrompt.into()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(ADD_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, ADD_SERIES_SELECTION_BLOCKS.len() - 1); + + AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::AddSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::AddSeries(None)) + ); + assert!(app.data.sonarr_data.add_series_modal.is_some()); + } + } + + #[test] + fn test_add_series_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if ADD_SERIES_BLOCKS.contains(&active_sonarr_block) { + assert!(AddSeriesHandler::accepts(active_sonarr_block)); + } else { + assert!(!AddSeriesHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_add_series_handler_is_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = true; + + let handler = AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AddSeriesPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_add_series_handler_is_ready_when_not_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = false; + + let handler = AddSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AddSeriesPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/library/delete_series_handler.rs b/src/handlers/sonarr_handlers/library/delete_series_handler.rs new file mode 100644 index 0000000..a9fbeff --- /dev/null +++ b/src/handlers/sonarr_handlers/library/delete_series_handler.rs @@ -0,0 +1,118 @@ +use crate::{ + app::{key_binding::DEFAULT_KEYBINDINGS, App}, + event::Key, + handlers::{handle_prompt_toggle, KeyEventHandler}, + models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}, + network::sonarr_network::SonarrEvent, +}; + +#[cfg(test)] +#[path = "delete_series_handler_tests.rs"] +mod delete_series_handler_tests; + +pub(super) struct DeleteSeriesHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<'a, 'b> { + fn accepts(active_block: ActiveSonarrBlock) -> bool { + DELETE_SERIES_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> Self { + DeleteSeriesHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + } + + fn handle_scroll_up(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + self.app.data.sonarr_data.selected_block.up(); + } + } + + fn handle_scroll_down(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + self.app.data.sonarr_data.selected_block.down(); + } + } + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + handle_prompt_toggle(self.app, self.key); + } + } + + fn handle_submit(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + match self.app.data.sonarr_data.selected_block.get_active_block() { + ActiveSonarrBlock::DeleteSeriesConfirmPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteSeries(None)); + self.app.should_refresh = true; + } else { + self.app.data.sonarr_data.reset_delete_series_preferences(); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::DeleteSeriesToggleDeleteFile => { + self.app.data.sonarr_data.delete_series_files = + !self.app.data.sonarr_data.delete_series_files; + } + ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion => { + self.app.data.sonarr_data.add_list_exclusion = + !self.app.data.sonarr_data.add_list_exclusion; + } + _ => (), + } + } + } + + fn handle_esc(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.reset_delete_series_preferences(); + self.app.data.sonarr_data.prompt_confirm = false; + } + } + + fn handle_char_key_event(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt + && self.app.data.sonarr_data.selected_block.get_active_block() + == ActiveSonarrBlock::DeleteSeriesConfirmPrompt + && self.key == DEFAULT_KEYBINDINGS.confirm.key + { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteSeries(None)); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } +} diff --git a/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs b/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs new file mode 100644 index 0000000..13469da --- /dev/null +++ b/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs @@ -0,0 +1,339 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::library::delete_series_handler::DeleteSeriesHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::sonarr::sonarr_data::DELETE_SERIES_SELECTION_BLOCKS; + use crate::models::BlockSelectionState; + + use super::*; + + #[rstest] + fn test_delete_series_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + DeleteSeriesHandler::with(key, &mut app, ActiveSonarrBlock::DeleteSeriesPrompt, None) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::DeleteSeriesToggleDeleteFile + ); + } else { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::DeleteSeriesConfirmPrompt + ); + } + } + + #[rstest] + fn test_delete_series_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + DeleteSeriesHandler::with(key, &mut app, ActiveSonarrBlock::DeleteSeriesPrompt, None) + .handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion + ); + } + } + + mod test_handle_left_right_action { + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + + DeleteSeriesHandler::with(key, &mut app, ActiveSonarrBlock::DeleteSeriesPrompt, None) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + DeleteSeriesHandler::with(key, &mut app, ActiveSonarrBlock::DeleteSeriesPrompt, None) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::models::servarr_data::sonarr::sonarr_data::DELETE_SERIES_SELECTION_BLOCKS; + use crate::models::BlockSelectionState; + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_delete_series_prompt_prompt_decline_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, DELETE_SERIES_SELECTION_BLOCKS.len() - 1); + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.data.sonarr_data.delete_series_files); + assert!(!app.data.sonarr_data.add_list_exclusion); + } + + #[test] + fn test_delete_series_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, DELETE_SERIES_SELECTION_BLOCKS.len() - 1); + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteSeries(None)) + ); + assert!(app.should_refresh); + assert!(app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.delete_series_files); + assert!(app.data.sonarr_data.add_list_exclusion); + } + + #[test] + fn test_delete_series_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::DeleteSeriesPrompt.into() + ); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert!(!app.should_refresh); + assert!(app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.delete_series_files); + assert!(app.data.sonarr_data.add_list_exclusion); + } + + #[test] + fn test_delete_series_toggle_delete_files_submit() { + let current_route = ActiveSonarrBlock::DeleteSeriesPrompt.into(); + let mut app = App::default(); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.sonarr_data.delete_series_files, true); + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.sonarr_data.delete_series_files, false); + } + } + + mod test_handle_esc { + use super::*; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_delete_series_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + + DeleteSeriesHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.data.sonarr_data.delete_series_files); + assert!(!app.data.sonarr_data.add_list_exclusion); + } + } + + mod test_handle_key_char { + use crate::{ + models::{ + servarr_data::sonarr::sonarr_data::DELETE_SERIES_SELECTION_BLOCKS, BlockSelectionState, + }, + network::sonarr_network::SonarrEvent, + }; + + use super::*; + + #[test] + fn test_delete_series_confirm_prompt_prompt_confirm() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, DELETE_SERIES_SELECTION_BLOCKS.len() - 1); + + DeleteSeriesHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteSeries(None)) + ); + assert!(app.should_refresh); + assert!(app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.delete_series_files); + assert!(app.data.sonarr_data.add_list_exclusion); + } + } + + #[test] + fn test_delete_series_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if DELETE_SERIES_BLOCKS.contains(&active_sonarr_block) { + assert!(DeleteSeriesHandler::accepts(active_sonarr_block)); + } else { + assert!(!DeleteSeriesHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_delete_series_handler_not_ready_when_loading() { + let mut app = App::default(); + app.is_loading = true; + + let handler = DeleteSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_delete_series_handler_ready_when_not_loading() { + let mut app = App::default(); + app.is_loading = false; + + let handler = DeleteSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/library/edit_series_handler.rs b/src/handlers/sonarr_handlers/library/edit_series_handler.rs new file mode 100644 index 0000000..dbab8c8 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/edit_series_handler.rs @@ -0,0 +1,404 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS}; +use crate::models::Scrollable; +use crate::network::sonarr_network::SonarrEvent; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; + +#[cfg(test)] +#[path = "edit_series_handler_tests.rs"] +mod edit_series_handler_tests; + +pub(super) struct EditSeriesHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EditSeriesHandler<'a, 'b> { + fn accepts(active_block: ActiveSonarrBlock) -> bool { + EDIT_SERIES_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> EditSeriesHandler<'a, 'b> { + EditSeriesHandler { + key, + app, + active_sonarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.sonarr_data.edit_series_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditSeriesSelectSeriesType => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .series_type_list + .scroll_up(), + ActiveSonarrBlock::EditSeriesSelectQualityProfile => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_up(), + ActiveSonarrBlock::EditSeriesSelectLanguageProfile => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .language_profile_list + .scroll_up(), + ActiveSonarrBlock::EditSeriesPrompt => self.app.data.sonarr_data.selected_block.up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditSeriesSelectSeriesType => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .series_type_list + .scroll_down(), + ActiveSonarrBlock::EditSeriesSelectQualityProfile => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_down(), + ActiveSonarrBlock::EditSeriesSelectLanguageProfile => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .language_profile_list + .scroll_down(), + ActiveSonarrBlock::EditSeriesPrompt => self.app.data.sonarr_data.selected_block.down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditSeriesSelectSeriesType => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .series_type_list + .scroll_to_top(), + ActiveSonarrBlock::EditSeriesSelectQualityProfile => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_top(), + ActiveSonarrBlock::EditSeriesSelectLanguageProfile => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .language_profile_list + .scroll_to_top(), + ActiveSonarrBlock::EditSeriesPathInput => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .path + .scroll_home(), + ActiveSonarrBlock::EditSeriesTagsInput => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .tags + .scroll_home(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditSeriesSelectSeriesType => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .series_type_list + .scroll_to_bottom(), + ActiveSonarrBlock::EditSeriesSelectQualityProfile => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_bottom(), + ActiveSonarrBlock::EditSeriesSelectLanguageProfile => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .language_profile_list + .scroll_to_bottom(), + ActiveSonarrBlock::EditSeriesPathInput => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .path + .reset_offset(), + ActiveSonarrBlock::EditSeriesTagsInput => self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .tags + .reset_offset(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditSeriesPrompt => handle_prompt_toggle(self.app, self.key), + ActiveSonarrBlock::EditSeriesPathInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveSonarrBlock::EditSeriesTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .tags + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditSeriesPrompt => { + match self.app.data.sonarr_data.selected_block.get_active_block() { + ActiveSonarrBlock::EditSeriesConfirmPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditSeries(None)); + self.app.should_refresh = true; + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::EditSeriesSelectSeriesType + | ActiveSonarrBlock::EditSeriesSelectQualityProfile + | ActiveSonarrBlock::EditSeriesSelectLanguageProfile => self.app.push_navigation_stack( + ( + self.app.data.sonarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ), + ActiveSonarrBlock::EditSeriesPathInput | ActiveSonarrBlock::EditSeriesTagsInput => { + self.app.push_navigation_stack( + ( + self.app.data.sonarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ); + self.app.should_ignore_quit_key = true; + } + ActiveSonarrBlock::EditSeriesToggleMonitored => { + self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .monitored = Some( + !self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .monitored + .unwrap_or_default(), + ) + } + ActiveSonarrBlock::EditSeriesToggleSeasonFolder => { + self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .use_season_folders = Some( + !self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .use_season_folders + .unwrap_or_default(), + ) + } + _ => (), + } + } + ActiveSonarrBlock::EditSeriesSelectSeriesType + | ActiveSonarrBlock::EditSeriesSelectQualityProfile + | ActiveSonarrBlock::EditSeriesSelectLanguageProfile => self.app.pop_navigation_stack(), + ActiveSonarrBlock::EditSeriesPathInput | ActiveSonarrBlock::EditSeriesTagsInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EditSeriesTagsInput | ActiveSonarrBlock::EditSeriesPathInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::EditSeriesPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.edit_series_modal = None; + self.app.data.sonarr_data.prompt_confirm = false; + } + ActiveSonarrBlock::EditSeriesSelectSeriesType + | ActiveSonarrBlock::EditSeriesSelectQualityProfile + | ActiveSonarrBlock::EditSeriesSelectLanguageProfile => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::EditSeriesPathInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveSonarrBlock::EditSeriesTagsInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .tags + ) + } + ActiveSonarrBlock::EditSeriesPrompt => { + if self.app.data.sonarr_data.selected_block.get_active_block() + == ActiveSonarrBlock::EditSeriesConfirmPrompt + && key == DEFAULT_KEYBINDINGS.confirm.key + { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::EditSeries(None)); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/library/edit_series_handler_tests.rs b/src/handlers/sonarr_handlers/library/edit_series_handler_tests.rs new file mode 100644 index 0000000..6b73c63 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/edit_series_handler_tests.rs @@ -0,0 +1,1343 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_str_eq; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::library::edit_series_handler::EditSeriesHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::modals::EditSeriesModal; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS}; + use crate::models::sonarr_models::SeriesType; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::modals::EditSeriesModal; + use crate::models::servarr_data::sonarr::sonarr_data::EDIT_SERIES_SELECTION_BLOCKS; + use crate::models::BlockSelectionState; + + use super::*; + + #[rstest] + fn test_edit_series_select_series_type_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let series_type_vec = Vec::from_iter(SeriesType::iter()); + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .series_type_list + .set_items(series_type_vec.clone()); + + if key == Key::Up { + for i in (0..series_type_vec.len()).rev() { + EditSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectSeriesType, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .series_type_list + .current_selection(), + &series_type_vec[i] + ); + } + } else { + for i in 0..series_type_vec.len() { + EditSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectSeriesType, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .series_type_list + .current_selection(), + &series_type_vec[(i + 1) % series_type_vec.len()] + ); + } + } + } + + #[rstest] + fn test_edit_series_select_quality_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + EditSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 2" + ); + + EditSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_edit_series_select_language_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .language_profile_list + .set_items(vec!["Test 1".to_owned(), "Test 2".to_owned()]); + + EditSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .language_profile_list + .current_selection(), + "Test 2" + ); + + EditSeriesHandler::with( + key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .language_profile_list + .current_selection(), + "Test 1" + ); + } + + #[rstest] + fn test_edit_series_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + EditSeriesHandler::with(key, &mut app, ActiveSonarrBlock::EditSeriesPrompt, None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditSeriesToggleMonitored + ); + } else { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditSeriesSelectQualityProfile + ); + } + } + + #[rstest] + fn test_edit_series_prompt_scroll_no_op_when_not_ready(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = true; + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + EditSeriesHandler::with(key, &mut app, ActiveSonarrBlock::EditSeriesPrompt, None).handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditSeriesToggleSeasonFolder + ); + } + } + + mod test_handle_home_end { + use std::sync::atomic::Ordering; + + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::modals::EditSeriesModal; + + use super::*; + + #[test] + fn test_edit_series_select_series_type_home_end() { + let series_type_vec = Vec::from_iter(SeriesType::iter()); + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .series_type_list + .set_items(series_type_vec.clone()); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectSeriesType, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .series_type_list + .current_selection(), + &series_type_vec[series_type_vec.len() - 1] + ); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectSeriesType, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .series_type_list + .current_selection(), + &series_type_vec[0] + ); + } + + #[test] + fn test_edit_series_select_quality_profile_scroll() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 3" + ); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_edit_series_select_language_profile_scroll() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .language_profile_list + .set_items(vec![ + "Test 1".to_owned(), + "Test 2".to_owned(), + "Test 3".to_owned(), + ]); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .language_profile_list + .current_selection(), + "Test 3" + ); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .language_profile_list + .current_selection(), + "Test 1" + ); + } + + #[test] + fn test_edit_series_path_input_home_end_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal { + path: "Test".into(), + ..EditSeriesModal::default() + }); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditSeriesPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditSeriesPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_series_tags_input_home_end_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal { + tags: "Test".into(), + ..EditSeriesModal::default() + }); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::EditSeriesTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 4 + ); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::EditSeriesTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_left_right_action { + use std::sync::atomic::Ordering; + + use crate::models::servarr_data::sonarr::modals::EditSeriesModal; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + EditSeriesHandler::with(key, &mut app, ActiveSonarrBlock::EditSeriesPrompt, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + EditSeriesHandler::with(key, &mut app, ActiveSonarrBlock::EditSeriesPrompt, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[test] + fn test_edit_series_path_input_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal { + path: "Test".into(), + ..EditSeriesModal::default() + }); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::EditSeriesPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::EditSeriesPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_edit_series_tags_input_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal { + tags: "Test".into(), + ..EditSeriesModal::default() + }); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::EditSeriesTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::EditSeriesTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::sonarr::modals::EditSeriesModal; + use crate::models::servarr_data::sonarr::sonarr_data::EDIT_SERIES_SELECTION_BLOCKS; + use crate::models::{BlockSelectionState, Route}; + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_series_path_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal { + path: "Test Path".into(), + ..EditSeriesModal::default() + }); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPathInput.into()); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPathInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .path + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditSeriesPrompt.into() + ); + } + + #[test] + fn test_edit_series_tags_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal { + tags: "Test Tags".into(), + ..EditSeriesModal::default() + }); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPathInput.into()); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesTagsInput, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .tags + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditSeriesPrompt.into() + ); + } + + #[test] + fn test_edit_series_prompt_prompt_decline_submit() { + let mut app = App::default(); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, EDIT_SERIES_SELECTION_BLOCKS.len() - 1); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + } + + #[test] + fn test_edit_series_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::default(); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, EDIT_SERIES_SELECTION_BLOCKS.len() - 1); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::EditSeries(None)) + ); + assert!(app.data.sonarr_data.edit_series_modal.is_some()); + assert!(app.should_refresh); + } + + #[test] + fn test_edit_series_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditSeriesPrompt.into() + ); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert!(!app.should_refresh); + } + + #[test] + fn test_edit_series_toggle_monitored_submit() { + let current_route = Route::from(( + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + )); + let mut app = App::default(); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(current_route); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .monitored, + Some(true) + ); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .monitored, + Some(false) + ); + } + + #[test] + fn test_edit_series_toggle_use_season_folders_submit() { + let current_route = Route::from(( + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + )); + let mut app = App::default(); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, 1); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(current_route); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .use_season_folders, + Some(true) + ); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .use_season_folders, + Some(false) + ); + } + + #[rstest] + #[case(ActiveSonarrBlock::EditSeriesSelectQualityProfile, 2)] + #[case(ActiveSonarrBlock::EditSeriesSelectLanguageProfile, 3)] + #[case(ActiveSonarrBlock::EditSeriesSelectSeriesType, 4)] + #[case(ActiveSonarrBlock::EditSeriesPathInput, 5)] + #[case(ActiveSonarrBlock::EditSeriesTagsInput, 6)] + fn test_edit_series_prompt_selected_block_submit( + #[case] selected_block: ActiveSonarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::default(); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack( + ( + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .into(), + ); + app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, y_index); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + (selected_block, Some(ActiveSonarrBlock::Series)).into() + ); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + + if selected_block == ActiveSonarrBlock::EditSeriesPathInput + || selected_block == ActiveSonarrBlock::EditSeriesTagsInput + { + assert!(app.should_ignore_quit_key); + } + } + + #[rstest] + fn test_edit_series_prompt_selected_block_submit_no_op_when_not_ready( + #[values(1, 2, 3, 4)] y_index: usize, + ) { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack( + ( + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .into(), + ); + app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.set_index(0, y_index); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ( + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .into() + ); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert!(!app.should_ignore_quit_key); + } + + #[rstest] + fn test_edit_series_prompt_selecting_preferences_blocks_submit( + #[values( + ActiveSonarrBlock::EditSeriesSelectSeriesType, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile, + ActiveSonarrBlock::EditSeriesPathInput, + ActiveSonarrBlock::EditSeriesTagsInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); + app.push_navigation_stack(active_sonarr_block.into()); + + EditSeriesHandler::with( + SUBMIT_KEY, + &mut app, + active_sonarr_block, + Some(ActiveSonarrBlock::Series), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditSeriesPrompt.into() + ); + + if active_sonarr_block == ActiveSonarrBlock::EditSeriesPathInput + || active_sonarr_block == ActiveSonarrBlock::EditSeriesTagsInput + { + assert!(!app.should_ignore_quit_key); + } + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::sonarr::modals::EditSeriesModal; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_edit_series_input_esc( + #[values( + ActiveSonarrBlock::EditSeriesTagsInput, + ActiveSonarrBlock::EditSeriesPathInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); + app.push_navigation_stack(active_sonarr_block.into()); + + EditSeriesHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EditSeriesPrompt.into() + ); + } + + #[test] + fn test_edit_series_prompt_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + + EditSeriesHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::EditSeriesPrompt, None) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + + assert!(app.data.sonarr_data.edit_series_modal.is_none()); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[rstest] + fn test_edit_series_esc( + #[values( + ActiveSonarrBlock::EditSeriesSelectSeriesType, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile + )] + active_sonarr_block: ActiveSonarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::default(); + app.is_loading = is_ready; + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(active_sonarr_block.into()); + + EditSeriesHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + } + + mod test_handle_key_char { + use super::*; + use crate::{ + models::{ + servarr_data::sonarr::{ + modals::EditSeriesModal, sonarr_data::EDIT_SERIES_SELECTION_BLOCKS, + }, + BlockSelectionState, + }, + network::sonarr_network::SonarrEvent, + }; + + #[test] + fn test_edit_series_path_input_backspace() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal { + path: "Test".into(), + ..EditSeriesModal::default() + }); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::EditSeriesPathInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .path + .text, + "Tes" + ); + } + + #[test] + fn test_edit_series_tags_input_backspace() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal { + tags: "Test".into(), + ..EditSeriesModal::default() + }); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::EditSeriesTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_edit_series_path_input_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + + EditSeriesHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::EditSeriesPathInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .path + .text, + "h" + ); + } + + #[test] + fn test_edit_series_tags_input_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + + EditSeriesHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::EditSeriesTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .tags + .text, + "h" + ); + } + + #[test] + fn test_edit_series_confirm_prompt_prompt_confirm() { + let mut app = App::default(); + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::EditSeriesPrompt.into()); + app.data.sonarr_data.selected_block = BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, EDIT_SERIES_SELECTION_BLOCKS.len() - 1); + + EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::EditSeries(None)) + ); + assert!(app.data.sonarr_data.edit_series_modal.is_some()); + assert!(app.should_refresh); + } + } + + #[test] + fn test_edit_series_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if EDIT_SERIES_BLOCKS.contains(&active_sonarr_block) { + assert!(EditSeriesHandler::accepts(active_sonarr_block)); + } else { + assert!(!EditSeriesHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_edit_series_handler_is_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = true; + + let handler = EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_series_handler_is_not_ready_when_edit_series_modal_is_none() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = false; + + let handler = EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_edit_series_handler_is_ready_when_edit_series_modal_is_some() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = false; + app.data.sonarr_data.edit_series_modal = Some(EditSeriesModal::default()); + + let handler = EditSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EditSeriesPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/library/episode_details_handler.rs b/src/handlers/sonarr_handlers/library/episode_details_handler.rs new file mode 100644 index 0000000..026fa11 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/episode_details_handler.rs @@ -0,0 +1,359 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::sonarr_handlers::library::season_details_handler::releases_sorting_options; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS}; +use crate::models::sonarr_models::{SonarrHistoryItem, SonarrRelease, SonarrReleaseDownloadBody}; +use crate::network::sonarr_network::SonarrEvent; + +#[cfg(test)] +#[path = "episode_details_handler_tests.rs"] +mod episode_details_handler_tests; + +pub(super) struct EpisodeDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> EpisodeDetailsHandler<'a, 'b> { + handle_table_events!( + self, + episode_history, + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("Season details modal is undefined") + .episode_details_modal + .as_mut() + .expect("Episode details modal is undefined") + .episode_history, + SonarrHistoryItem + ); + handle_table_events!( + self, + episode_releases, + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("Season details modal is undefined") + .episode_details_modal + .as_mut() + .expect("Episode details modal is undefined") + .episode_releases, + SonarrRelease + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for EpisodeDetailsHandler<'a, 'b> { + fn handle(&mut self) { + let episode_history_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::EpisodeHistory.into()); + let episode_releases_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::ManualEpisodeSearch.into()) + .sorting_block(ActiveSonarrBlock::ManualEpisodeSearchSortPrompt.into()) + .sort_options(releases_sorting_options()); + + if !self.handle_episode_history_table_events(episode_history_table_handling_config) + && !self.handle_episode_releases_table_events(episode_releases_table_handling_config) + { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + EPISODE_DETAILS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, + ) -> Self { + Self { + key, + app, + active_sonarr_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + && if let Some(season_details_modal) = self.app.data.sonarr_data.season_details_modal.as_ref() + { + if let Some(episode_details_modal) = &season_details_modal.episode_details_modal { + match self.active_sonarr_block { + ActiveSonarrBlock::EpisodeHistory => !episode_details_modal.episode_history.is_empty(), + ActiveSonarrBlock::ManualEpisodeSearch => { + !episode_details_modal.episode_releases.is_empty() + } + _ => true, + } + } else { + false + } + } else { + false + } + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EpisodeDetails + | ActiveSonarrBlock::EpisodeHistory + | ActiveSonarrBlock::EpisodeFile + | ActiveSonarrBlock::ManualEpisodeSearch => match self.key { + _ if self.key == DEFAULT_KEYBINDINGS.left.key => { + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap() + .episode_details_tabs + .previous(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_details_tabs + .get_active_route(), + ); + } + _ if self.key == DEFAULT_KEYBINDINGS.right.key => { + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap() + .episode_details_tabs + .next(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_details_tabs + .get_active_route(), + ); + } + _ => (), + }, + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt + | ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt => { + handle_prompt_toggle(self.app, self.key); + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EpisodeHistory => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::EpisodeHistoryDetails.into()); + } + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::TriggerAutomaticEpisodeSearch(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::ManualEpisodeSearch => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt.into()); + } + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + let SonarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_releases + .current_selection(); + let episode_id = self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episodes + .current_selection() + .id; + let params = SonarrReleaseDownloadBody { + guid: guid.clone(), + indexer_id: *indexer_id, + episode_id: Some(episode_id), + ..SonarrReleaseDownloadBody::default() + }; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DownloadRelease(params)); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::EpisodeDetails + | ActiveSonarrBlock::EpisodeFile + | ActiveSonarrBlock::EpisodeHistory + | ActiveSonarrBlock::ManualEpisodeSearch => { + self.app.pop_navigation_stack(); + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal = None; + } + ActiveSonarrBlock::EpisodeHistoryDetails => { + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt + | ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::EpisodeDetails + | ActiveSonarrBlock::EpisodeHistory + | ActiveSonarrBlock::EpisodeFile + | ActiveSonarrBlock::ManualEpisodeSearch => match self.key { + _ if self.key == DEFAULT_KEYBINDINGS.refresh.key => { + self + .app + .pop_and_push_navigation_stack(self.active_sonarr_block.into()); + } + _ if self.key == DEFAULT_KEYBINDINGS.auto_search.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchEpisodePrompt.into()); + } + _ => (), + }, + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt + if key == DEFAULT_KEYBINDINGS.confirm.key => + { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::TriggerAutomaticEpisodeSearch(None)); + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt + if key == DEFAULT_KEYBINDINGS.confirm.key => + { + if self.app.data.sonarr_data.prompt_confirm { + let SonarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_releases + .current_selection(); + let episode_id = self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episodes + .current_selection() + .id; + let params = SonarrReleaseDownloadBody { + guid: guid.clone(), + indexer_id: *indexer_id, + episode_id: Some(episode_id), + ..SonarrReleaseDownloadBody::default() + }; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DownloadRelease(params)); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/library/episode_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/episode_details_handler_tests.rs new file mode 100644 index 0000000..c5d31f3 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/episode_details_handler_tests.rs @@ -0,0 +1,771 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::handlers::sonarr_handlers::library::episode_details_handler::EpisodeDetailsHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS, + }; + use crate::models::sonarr_models::SonarrReleaseDownloadBody; + use crate::models::stateful_table::StatefulTable; + use rstest::rstest; + use strum::IntoEnumIterator; + + mod test_handle_left_right_actions { + use super::*; + use crate::event::Key; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_left_right_prompt_toggle( + #[values( + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt, + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); + + EpisodeDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + EpisodeDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[rstest] + #[case(ActiveSonarrBlock::EpisodeDetails, ActiveSonarrBlock::EpisodeHistory)] + #[case(ActiveSonarrBlock::EpisodeHistory, ActiveSonarrBlock::EpisodeFile)] + #[case(ActiveSonarrBlock::EpisodeFile, ActiveSonarrBlock::ManualEpisodeSearch)] + #[case( + ActiveSonarrBlock::ManualEpisodeSearch, + ActiveSonarrBlock::EpisodeDetails + )] + fn test_episode_details_tabs_left_right_action( + #[case] left_block: ActiveSonarrBlock, + #[case] right_block: ActiveSonarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + app.is_loading = is_ready; + app.push_navigation_stack(right_block.into()); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap() + .episode_details_tabs + .index = app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_details_tabs + .tabs + .iter() + .position(|tab_route| tab_route.route == right_block.into()) + .unwrap_or_default(); + + EpisodeDetailsHandler::with(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None) + .handle(); + + assert_eq!( + app.get_current_route(), + app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_details_tabs + .get_active_route() + ); + assert_eq!(app.get_current_route(), left_block.into()); + + EpisodeDetailsHandler::with(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None) + .handle(); + + assert_eq!( + app.get_current_route(), + app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_details_tabs + .get_active_route() + ); + assert_eq!(app.get_current_route(), right_block.into()); + } + } + + mod test_handle_submit { + use super::*; + use crate::event::Key; + use crate::models::stateful_table::StatefulTable; + use crate::network::sonarr_network::SonarrEvent; + use pretty_assertions::assert_eq; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_episode_history_submit() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + + EpisodeDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EpisodeHistory, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EpisodeHistoryDetails.into() + ); + } + + #[test] + fn test_episode_history_submit_no_op_when_episode_history_is_empty() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap() + .episode_history = StatefulTable::default(); + app.push_navigation_stack(ActiveSonarrBlock::EpisodeHistory.into()); + + EpisodeDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EpisodeHistory, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EpisodeHistory.into() + ); + } + + #[test] + fn test_episode_history_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::EpisodeHistory.into()); + + EpisodeDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::EpisodeHistory, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EpisodeHistory.into() + ); + } + + #[rstest] + #[case( + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt, + SonarrEvent::TriggerAutomaticEpisodeSearch(None) + )] + fn test_episode_details_prompt_confirm_submit( + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + #[values( + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::EpisodeFile, + ActiveSonarrBlock::ManualEpisodeSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.push_navigation_stack(prompt_block.into()); + + EpisodeDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + } + + #[test] + fn test_manual_episode_search_confirm_prompt_confirm_submit() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::ManualEpisodeSearch.into()); + app.push_navigation_stack(ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt.into()); + + EpisodeDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::ManualEpisodeSearch.into() + ); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DownloadRelease(SonarrReleaseDownloadBody { + guid: String::new(), + indexer_id: 0, + episode_id: Some(0), + ..SonarrReleaseDownloadBody::default() + })) + ); + } + + #[rstest] + fn test_episode_details_prompt_decline_submit( + #[values( + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt, + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt + )] + prompt_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + EpisodeDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EpisodeDetails.into() + ); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + } + + #[test] + fn test_manual_episode_search_submit() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::ManualEpisodeSearch.into()); + + EpisodeDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::ManualEpisodeSearch, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt.into() + ); + } + + #[test] + fn test_manual_episode_search_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::ManualEpisodeSearch.into()); + + EpisodeDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::ManualEpisodeSearch, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::ManualEpisodeSearch.into() + ); + } + } + + mod test_handle_esc { + use super::*; + use crate::event::Key; + use pretty_assertions::assert_eq; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_episode_history_details_block_esc() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::EpisodeHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::EpisodeHistoryDetails.into()); + + EpisodeDetailsHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::EpisodeHistoryDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EpisodeHistory.into() + ); + } + + #[rstest] + fn test_episode_details_prompts_esc( + #[values( + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt, + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt + )] + prompt_block: ActiveSonarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.is_loading = is_ready; + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + EpisodeDetailsHandler::with(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EpisodeDetails.into() + ); + } + + #[rstest] + fn test_episode_details_tabs_esc( + #[values( + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::EpisodeFile, + ActiveSonarrBlock::ManualEpisodeSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + app.push_navigation_stack(active_sonarr_block.into()); + + EpisodeDetailsHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + assert!(app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .is_none()); + } + } + + mod test_handle_key_char { + use super::*; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::network::sonarr_network::SonarrEvent; + use pretty_assertions::assert_eq; + + #[rstest] + fn test_auto_search_key( + #[values( + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::EpisodeFile, + ActiveSonarrBlock::ManualEpisodeSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(active_sonarr_block.into()); + + EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt.into() + ); + } + + #[rstest] + fn test_auto_search_key_no_op_when_not_ready( + #[values( + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::EpisodeFile, + ActiveSonarrBlock::ManualEpisodeSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + + EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + } + + #[rstest] + fn test_refresh_key( + #[values( + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::EpisodeFile, + ActiveSonarrBlock::ManualEpisodeSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(active_sonarr_block.into()); + app.is_routing = false; + + EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(app.is_routing); + } + + #[rstest] + fn test_refresh_key_no_op_when_not_ready( + #[values( + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::EpisodeFile, + ActiveSonarrBlock::ManualEpisodeSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.is_routing = false; + + EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(!app.is_routing); + } + + #[rstest] + fn test_episode_details_prompt_confirm_confirm_key( + #[values( + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::EpisodeFile, + ActiveSonarrBlock::ManualEpisodeSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchEpisodePrompt.into()); + + EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::TriggerAutomaticEpisodeSearch(None)) + ); + } + + #[test] + fn test_episode_details_manual_search_confirm_prompt_confirm_confirm_key() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::ManualEpisodeSearch.into()); + app.push_navigation_stack(ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt.into()); + + EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::ManualEpisodeSearch.into() + ); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DownloadRelease(SonarrReleaseDownloadBody { + guid: String::new(), + indexer_id: 0, + episode_id: Some(0), + ..SonarrReleaseDownloadBody::default() + })) + ); + } + } + + #[test] + fn test_episode_details_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if EPISODE_DETAILS_BLOCKS.contains(&active_sonarr_block) { + assert!(EpisodeDetailsHandler::accepts(active_sonarr_block)); + } else { + assert!(!EpisodeDetailsHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_episode_details_handler_is_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); + app.is_loading = true; + + let handler = EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EpisodeDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_episode_details_handler_is_not_ready_when_season_details_modal_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); + app.is_loading = false; + + let handler = EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EpisodeDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_episode_details_handler_is_not_ready_when_episode_details_modal_is_empty() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal = None; + app.push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); + app.is_loading = false; + + let handler = EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EpisodeDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_episode_details_handler_is_not_ready_when_episode_history_table_is_empty() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap() + .episode_history = StatefulTable::default(); + app.push_navigation_stack(ActiveSonarrBlock::EpisodeHistory.into()); + app.is_loading = false; + + let handler = EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::EpisodeHistory, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_episode_details_handler_is_not_ready_when_episode_releases_table_is_empty() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap() + .episode_releases = StatefulTable::default(); + app.push_navigation_stack(ActiveSonarrBlock::ManualEpisodeSearch.into()); + app.is_loading = false; + + let handler = EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::ManualEpisodeSearch, + None, + ); + + assert!(!handler.is_ready()); + } + + #[rstest] + fn test_episode_details_handler_is_ready_with_empty_tables_for_details_and_file_routes( + #[values(ActiveSonarrBlock::EpisodeDetails, ActiveSonarrBlock::EpisodeFile)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal = Some(EpisodeDetailsModal::default()); + app.push_navigation_stack(active_sonarr_block.into()); + app.is_loading = false; + + let handler = EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + active_sonarr_block, + None, + ); + + assert!(handler.is_ready()); + } + + #[rstest] + fn test_episode_details_handler_is_ready( + #[values( + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeFile, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::ManualEpisodeSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(active_sonarr_block.into()); + app.is_loading = false; + + let handler = EpisodeDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + active_sonarr_block, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/library/library_handler_tests.rs b/src/handlers/sonarr_handlers/library/library_handler_tests.rs new file mode 100644 index 0000000..c79a107 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/library_handler_tests.rs @@ -0,0 +1,929 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use std::cmp::Ordering; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::library::{series_sorting_options, LibraryHandler}; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, + EPISODE_DETAILS_BLOCKS, LIBRARY_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS, + }; + use crate::models::sonarr_models::{Series, SeriesStatus, SeriesType}; + use crate::test_handler_delegation; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use crate::assert_delete_prompt; + use crate::models::servarr_data::sonarr::sonarr_data::DELETE_SERIES_SELECTION_BLOCKS; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_series_delete() { + let mut app = App::default(); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + assert_delete_prompt!( + LibraryHandler, + app, + ActiveSonarrBlock::Series, + ActiveSonarrBlock::DeleteSeriesPrompt + ); + assert_eq!( + app.data.sonarr_data.selected_block.blocks, + DELETE_SERIES_SELECTION_BLOCKS + ); + } + + #[test] + fn test_series_delete_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + LibraryHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::Series, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_series_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(0); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::System.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + } + + #[rstest] + fn test_series_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(0); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Downloads.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Downloads.into()); + } + + #[rstest] + fn test_left_right_update_all_series_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + LibraryHandler::with( + key, + &mut app, + ActiveSonarrBlock::UpdateAllSeriesPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + LibraryHandler::with( + key, + &mut app, + ActiveSonarrBlock::UpdateAllSeriesPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_series_details_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::Series, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + } + + #[test] + fn test_series_details_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::Series, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + + #[test] + fn test_update_all_series_prompt_confirm_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::UpdateAllSeriesPrompt.into()); + + LibraryHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::UpdateAllSeriesPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::UpdateAllSeries) + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + + #[test] + fn test_update_all_series_prompt_decline_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::UpdateAllSeriesPrompt.into()); + + LibraryHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::UpdateAllSeriesPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_update_all_series_prompt_blocks_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::UpdateAllSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + + LibraryHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::UpdateAllSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data = create_test_sonarr_data(); + + LibraryHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::Series, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::Number; + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::models::servarr_data::sonarr::sonarr_data::SonarrData; + use crate::models::sonarr_models::SeriesType; + + use crate::network::sonarr_network::SonarrEvent; + use crate::test_edit_series_key; + + use super::*; + + #[test] + fn test_series_add_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddSeriesSearchInput.into() + ); + assert!(app.should_ignore_quit_key); + assert!(app.data.sonarr_data.add_series_search.is_some()); + } + + #[test] + fn test_series_add_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert!(!app.should_ignore_quit_key); + assert!(app.data.sonarr_data.add_series_search.is_none()); + } + + #[test] + fn test_series_edit_key() { + test_edit_series_key!( + LibraryHandler, + ActiveSonarrBlock::Series, + ActiveSonarrBlock::Series + ); + } + + #[test] + fn test_series_edit_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.edit.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert!(app.data.sonarr_data.edit_series_modal.is_none()); + } + + #[test] + fn test_update_all_series_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::UpdateAllSeriesPrompt.into() + ); + } + + #[test] + fn test_update_all_series_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + + #[test] + fn test_refresh_series_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_series_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_update_all_series_prompt_confirm() { + let mut app = App::default(); + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::UpdateAllSeriesPrompt.into()); + + LibraryHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::UpdateAllSeriesPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::UpdateAllSeries) + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + } + + #[rstest] + fn test_delegates_add_series_blocks_to_add_series_handler( + #[values( + ActiveSonarrBlock::AddSeriesAlreadyInLibrary, + ActiveSonarrBlock::AddSeriesEmptySearchResults, + ActiveSonarrBlock::AddSeriesPrompt, + ActiveSonarrBlock::AddSeriesSearchInput, + ActiveSonarrBlock::AddSeriesSearchResults, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + ActiveSonarrBlock::AddSeriesSelectMonitor, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + ActiveSonarrBlock::AddSeriesSelectRootFolder, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + ActiveSonarrBlock::AddSeriesTagsInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + LibraryHandler, + ActiveSonarrBlock::Series, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_series_details_blocks_to_series_details_handler( + #[values( + ActiveSonarrBlock::SeriesDetails, + ActiveSonarrBlock::SeriesHistory, + ActiveSonarrBlock::SearchSeason, + ActiveSonarrBlock::SearchSeasonError, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt, + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + ActiveSonarrBlock::SearchSeriesHistory, + ActiveSonarrBlock::SearchSeriesHistoryError, + ActiveSonarrBlock::FilterSeriesHistory, + ActiveSonarrBlock::FilterSeriesHistoryError, + ActiveSonarrBlock::SeriesHistorySortPrompt, + ActiveSonarrBlock::SeriesHistoryDetails + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + LibraryHandler, + ActiveSonarrBlock::Series, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_season_details_blocks_to_season_details_handler( + #[values( + ActiveSonarrBlock::SeasonDetails, + ActiveSonarrBlock::SeasonHistory, + ActiveSonarrBlock::SearchEpisodes, + ActiveSonarrBlock::SearchEpisodesError, + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, + ActiveSonarrBlock::SearchSeasonHistory, + ActiveSonarrBlock::SearchSeasonHistoryError, + ActiveSonarrBlock::FilterSeasonHistory, + ActiveSonarrBlock::FilterSeasonHistoryError, + ActiveSonarrBlock::SeasonHistorySortPrompt, + ActiveSonarrBlock::SeasonHistoryDetails, + ActiveSonarrBlock::ManualSeasonSearch, + ActiveSonarrBlock::ManualSeasonSearchSortPrompt, + ActiveSonarrBlock::DeleteEpisodeFilePrompt + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + LibraryHandler, + ActiveSonarrBlock::Series, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_episode_details_blocks_to_season_details_handler( + #[values( + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt, + ActiveSonarrBlock::EpisodeHistoryDetails, + ActiveSonarrBlock::ManualEpisodeSearch, + ActiveSonarrBlock::ManualEpisodeSearchSortPrompt, + ActiveSonarrBlock::DeleteEpisodeFilePrompt + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + LibraryHandler, + ActiveSonarrBlock::Series, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_edit_series_blocks_to_edit_series_handler( + #[values( + ActiveSonarrBlock::EditSeriesPrompt, + ActiveSonarrBlock::EditSeriesPathInput, + ActiveSonarrBlock::EditSeriesSelectSeriesType, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile, + ActiveSonarrBlock::EditSeriesTagsInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + LibraryHandler, + ActiveSonarrBlock::Series, + active_sonarr_block + ); + } + + #[test] + fn test_delegates_delete_series_blocks_to_delete_series_handler() { + test_handler_delegation!( + LibraryHandler, + ActiveSonarrBlock::Series, + ActiveSonarrBlock::DeleteSeriesPrompt + ); + } + + #[test] + fn test_series_sorting_options_title() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = |a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[0].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Title"); + } + + #[test] + fn test_series_sorting_options_year() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = |a, b| a.year.cmp(&b.year); + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[1].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Year"); + } + + #[test] + fn test_series_sorting_options_network() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = |a, b| { + a.network + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp(&b.network.as_ref().unwrap_or(&String::new()).to_lowercase()) + }; + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[2].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Network"); + } + + #[test] + fn test_series_sorting_options_status() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = |a, b| { + a.status + .to_string() + .to_lowercase() + .cmp(&b.status.to_string().to_lowercase()) + }; + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[3].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Status"); + } + + #[test] + fn test_series_sorting_options_rating() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = |a, b| { + a.certification + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp( + &b.certification + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase(), + ) + }; + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[4].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Rating"); + } + + #[test] + fn test_series_sorting_options_type() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = |a, b| { + a.series_type + .to_string() + .to_lowercase() + .cmp(&b.series_type.to_string().to_lowercase()) + }; + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[5].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Type"); + } + + #[test] + fn test_series_sorting_options_quality() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = + |a, b| a.quality_profile_id.cmp(&b.quality_profile_id); + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[6].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_series_sorting_options_language() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = + |a, b| a.language_profile_id.cmp(&b.language_profile_id); + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[7].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Language"); + } + + #[test] + fn test_series_sorting_options_monitored() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = |a, b| a.monitored.cmp(&b.monitored); + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[8].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Monitored"); + } + + #[test] + fn test_series_sorting_options_tags() { + let expected_cmp_fn: fn(&Series, &Series) -> Ordering = |a, b| { + let a_str = a + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + let b_str = b + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + + a_str.cmp(&b_str) + }; + let mut expected_series_vec = series_vec(); + expected_series_vec.sort_by(expected_cmp_fn); + + let sort_option = series_sorting_options()[9].clone(); + let mut sorted_series_vec = series_vec(); + sorted_series_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_series_vec, expected_series_vec); + assert_str_eq!(sort_option.name, "Tags"); + } + + #[test] + fn test_library_handler_accepts() { + let mut library_handler_blocks = Vec::new(); + library_handler_blocks.extend(LIBRARY_BLOCKS); + library_handler_blocks.extend(ADD_SERIES_BLOCKS); + library_handler_blocks.extend(DELETE_SERIES_BLOCKS); + library_handler_blocks.extend(EDIT_SERIES_BLOCKS); + library_handler_blocks.extend(SERIES_DETAILS_BLOCKS); + library_handler_blocks.extend(SEASON_DETAILS_BLOCKS); + library_handler_blocks.extend(EPISODE_DETAILS_BLOCKS); + + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if library_handler_blocks.contains(&active_sonarr_block) { + assert!(LibraryHandler::accepts(active_sonarr_block)); + } else { + assert!(!LibraryHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_library_handler_not_ready_when_loading() { + let mut app = App::default(); + app.is_loading = true; + + let handler = LibraryHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_library_handler_not_ready_when_series_is_empty() { + let mut app = App::default(); + app.is_loading = false; + + let handler = LibraryHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_library_handler_ready_when_not_loading_and_series_is_not_empty() { + let mut app = App::default(); + app.is_loading = false; + app + .data + .sonarr_data + .series + .set_items(vec![Series::default()]); + + let handler = LibraryHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::Series, + None, + ); + + assert!(handler.is_ready()); + } + + fn series_vec() -> Vec { + vec![ + Series { + id: 3, + title: "test 1".into(), + network: Some("Network 1".to_owned()), + year: 2024, + monitored: false, + season_folder: false, + status: SeriesStatus::Ended, + quality_profile_id: 1, + language_profile_id: 1, + certification: Some("TV-MA".to_owned()), + series_type: SeriesType::Daily, + tags: vec![1.into(), 2.into()], + ..Series::default() + }, + Series { + id: 2, + title: "test 2".into(), + network: Some("Network 2".to_owned()), + year: 1998, + monitored: false, + season_folder: false, + status: SeriesStatus::Continuing, + quality_profile_id: 2, + language_profile_id: 2, + certification: Some("TV-PG".to_owned()), + series_type: SeriesType::Anime, + tags: vec![1.into(), 3.into()], + ..Series::default() + }, + Series { + id: 1, + title: "test 3".into(), + network: Some("network 3".to_owned()), + year: 1954, + monitored: true, + season_folder: false, + status: SeriesStatus::Upcoming, + quality_profile_id: 3, + language_profile_id: 3, + certification: Some("TV-G".to_owned()), + tags: vec![2.into(), 3.into()], + series_type: SeriesType::Standard, + ..Series::default() + }, + ] + } +} diff --git a/src/handlers/sonarr_handlers/library/mod.rs b/src/handlers/sonarr_handlers/library/mod.rs new file mode 100644 index 0000000..ef1f00c --- /dev/null +++ b/src/handlers/sonarr_handlers/library/mod.rs @@ -0,0 +1,312 @@ +use add_series_handler::AddSeriesHandler; +mod edit_series_handler; +use delete_series_handler::DeleteSeriesHandler; +use edit_series_handler::EditSeriesHandler; + +use crate::{ + app::App, + event::Key, + handle_table_events, + handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}, + models::{ + servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, DELETE_SERIES_SELECTION_BLOCKS, EDIT_SERIES_SELECTION_BLOCKS, + LIBRARY_BLOCKS, + }, + sonarr_models::Series, + stateful_table::SortOption, + BlockSelectionState, HorizontallyScrollableText, + }, + network::sonarr_network::SonarrEvent, +}; + +use super::handle_change_tab_left_right_keys; +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::handlers::sonarr_handlers::library::episode_details_handler::EpisodeDetailsHandler; +use crate::handlers::sonarr_handlers::library::season_details_handler::SeasonDetailsHandler; +use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler; +use crate::handlers::table_handler::TableHandlingConfig; + +mod add_series_handler; +mod delete_series_handler; + +mod episode_details_handler; +#[cfg(test)] +#[path = "library_handler_tests.rs"] +mod library_handler_tests; +mod season_details_handler; +mod series_details_handler; + +pub(super) struct LibraryHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + context: Option, +} + +impl<'a, 'b> LibraryHandler<'a, 'b> { + handle_table_events!(self, series, self.app.data.sonarr_data.series, Series); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, 'b> { + fn handle(&mut self) { + let series_table_handling_config = TableHandlingConfig::new(ActiveSonarrBlock::Series.into()) + .sorting_block(ActiveSonarrBlock::SeriesSortPrompt.into()) + .sort_by_fn(|a: &Series, b: &Series| a.id.cmp(&b.id)) + .sort_options(series_sorting_options()) + .searching_block(ActiveSonarrBlock::SearchSeries.into()) + .search_error_block(ActiveSonarrBlock::SearchSeriesError.into()) + .search_field_fn(|series| &series.title.text) + .filtering_block(ActiveSonarrBlock::FilterSeries.into()) + .filter_error_block(ActiveSonarrBlock::FilterSeriesError.into()) + .filter_field_fn(|series| &series.title.text); + + if !self.handle_series_table_events(series_table_handling_config) { + match self.active_sonarr_block { + _ if AddSeriesHandler::accepts(self.active_sonarr_block) => { + AddSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ if DeleteSeriesHandler::accepts(self.active_sonarr_block) => { + DeleteSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ if EditSeriesHandler::accepts(self.active_sonarr_block) => { + EditSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ if SeriesDetailsHandler::accepts(self.active_sonarr_block) => { + SeriesDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ if SeasonDetailsHandler::accepts(self.active_sonarr_block) => { + SeasonDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ if EpisodeDetailsHandler::accepts(self.active_sonarr_block) => { + EpisodeDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } + _ => self.handle_key_event(), + } + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + AddSeriesHandler::accepts(active_block) + || DeleteSeriesHandler::accepts(active_block) + || EditSeriesHandler::accepts(active_block) + || SeriesDetailsHandler::accepts(active_block) + || SeasonDetailsHandler::accepts(active_block) + || EpisodeDetailsHandler::accepts(active_block) + || LIBRARY_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> LibraryHandler<'a, 'b> { + LibraryHandler { + key, + app, + active_sonarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.sonarr_data.series.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::Series { + self + .app + .push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::Series => handle_change_tab_left_right_keys(self.app, self.key), + ActiveSonarrBlock::UpdateAllSeriesPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::Series => self + .app + .push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()), + ActiveSonarrBlock::UpdateAllSeriesPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::UpdateAllSeries); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::UpdateAllSeriesPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + _ => { + handle_clear_errors(self.app); + } + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::Series => match self.key { + _ if key == DEFAULT_KEYBINDINGS.edit.key => { + self.app.push_navigation_stack( + ( + ActiveSonarrBlock::EditSeriesPrompt, + Some(ActiveSonarrBlock::Series), + ) + .into(), + ); + self.app.data.sonarr_data.edit_series_modal = Some((&self.app.data.sonarr_data).into()); + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + } + _ if key == DEFAULT_KEYBINDINGS.add.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AddSeriesSearchInput.into()); + self.app.data.sonarr_data.add_series_search = Some(HorizontallyScrollableText::default()); + self.app.should_ignore_quit_key = true; + } + _ if key == DEFAULT_KEYBINDINGS.update.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::UpdateAllSeriesPrompt.into()); + } + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ => (), + }, + ActiveSonarrBlock::UpdateAllSeriesPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::UpdateAllSeries); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } +} + +fn series_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Title", + cmp_fn: Some(|a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }), + }, + SortOption { + name: "Year", + cmp_fn: Some(|a, b| a.year.cmp(&b.year)), + }, + SortOption { + name: "Network", + cmp_fn: Some(|a, b| { + a.network + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp(&b.network.as_ref().unwrap_or(&String::new()).to_lowercase()) + }), + }, + SortOption { + name: "Status", + cmp_fn: Some(|a, b| { + a.status + .to_string() + .to_lowercase() + .cmp(&b.status.to_string().to_lowercase()) + }), + }, + SortOption { + name: "Rating", + cmp_fn: Some(|a, b| { + a.certification + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp( + &b.certification + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase(), + ) + }), + }, + SortOption { + name: "Type", + cmp_fn: Some(|a, b| a.series_type.to_string().cmp(&b.series_type.to_string())), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| a.quality_profile_id.cmp(&b.quality_profile_id)), + }, + SortOption { + name: "Language", + cmp_fn: Some(|a, b| a.language_profile_id.cmp(&b.language_profile_id)), + }, + SortOption { + name: "Monitored", + cmp_fn: Some(|a, b| a.monitored.cmp(&b.monitored)), + }, + SortOption { + name: "Tags", + cmp_fn: Some(|a, b| { + let a_str = a + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + let b_str = b + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + + a_str.cmp(&b_str) + }), + }, + ] +} diff --git a/src/handlers/sonarr_handlers/library/season_details_handler.rs b/src/handlers/sonarr_handlers/library/season_details_handler.rs new file mode 100644 index 0000000..c66ab09 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/season_details_handler.rs @@ -0,0 +1,491 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::sonarr_handlers::history::history_sorting_options; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS}; +use crate::models::servarr_models::Language; +use crate::models::sonarr_models::{ + Episode, SonarrHistoryItem, SonarrRelease, SonarrReleaseDownloadBody, +}; +use crate::models::stateful_table::SortOption; +use crate::network::sonarr_network::SonarrEvent; +use serde_json::Number; + +#[cfg(test)] +#[path = "season_details_handler_tests.rs"] +mod season_details_handler_tests; + +pub(super) struct SeasonDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> SeasonDetailsHandler<'a, 'b> { + handle_table_events!( + self, + episodes, + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("Season details modal is undefined") + .episodes, + Episode + ); + handle_table_events!( + self, + season_history, + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("Season details modal is undefined") + .season_history, + SonarrHistoryItem + ); + handle_table_events!( + self, + season_releases, + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("Season details modal is undefined") + .season_releases, + SonarrRelease + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler<'a, 'b> { + fn handle(&mut self) { + let episodes_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::SeasonDetails.into()) + .searching_block(ActiveSonarrBlock::SearchEpisodes.into()) + .search_error_block(ActiveSonarrBlock::SearchEpisodesError.into()) + .search_field_fn(|episode: &Episode| &episode.title); + let season_history_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::SeasonHistory.into()) + .sorting_block(ActiveSonarrBlock::SeasonHistorySortPrompt.into()) + .sort_options(history_sorting_options()) + .sort_by_fn(|a: &SonarrHistoryItem, b: &SonarrHistoryItem| a.id.cmp(&b.id)) + .searching_block(ActiveSonarrBlock::SearchSeasonHistory.into()) + .search_error_block(ActiveSonarrBlock::SearchSeasonHistoryError.into()) + .search_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text) + .filtering_block(ActiveSonarrBlock::FilterSeasonHistory.into()) + .filter_error_block(ActiveSonarrBlock::FilterSeasonHistoryError.into()) + .filter_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text); + let season_releases_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::ManualSeasonSearch.into()) + .sorting_block(ActiveSonarrBlock::ManualSeasonSearchSortPrompt.into()) + .sort_options(releases_sorting_options()); + + if !self.handle_episodes_table_events(episodes_table_handling_config) + && !self.handle_season_history_table_events(season_history_table_handling_config) + && !self.handle_season_releases_table_events(season_releases_table_handling_config) + { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + SEASON_DETAILS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> SeasonDetailsHandler<'a, 'b> { + SeasonDetailsHandler { + key, + app, + active_sonarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + && if let Some(season_details_modal) = &self.app.data.sonarr_data.season_details_modal { + match self.active_sonarr_block { + ActiveSonarrBlock::SeasonDetails => !season_details_modal.episodes.is_empty(), + ActiveSonarrBlock::SeasonHistory => !season_details_modal.season_history.is_empty(), + ActiveSonarrBlock::ManualSeasonSearch => !season_details_modal.season_releases.is_empty(), + _ => true, + } + } else { + false + } + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::SeasonDetails { + self + .app + .push_navigation_stack(ActiveSonarrBlock::DeleteEpisodeFilePrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeasonDetails + | ActiveSonarrBlock::SeasonHistory + | ActiveSonarrBlock::ManualSeasonSearch => match self.key { + _ if self.key == DEFAULT_KEYBINDINGS.left.key => { + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_details_tabs + .previous(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_details_tabs + .get_active_route(), + ); + } + _ if self.key == DEFAULT_KEYBINDINGS.right.key => { + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_details_tabs + .next(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_details_tabs + .get_active_route(), + ); + } + _ => (), + }, + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt + | ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt + | ActiveSonarrBlock::DeleteEpisodeFilePrompt => { + handle_prompt_toggle(self.app, self.key); + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeasonDetails + if self.app.data.sonarr_data.season_details_modal.is_some() + && !self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episodes + .is_empty() => + { + self + .app + .push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()) + } + ActiveSonarrBlock::SeasonHistory => self + .app + .push_navigation_stack(ActiveSonarrBlock::SeasonHistoryDetails.into()), + ActiveSonarrBlock::DeleteEpisodeFilePrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteEpisodeFile(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::TriggerAutomaticSeasonSearch(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::ManualSeasonSearch => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into()); + } + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + let SonarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_releases + .current_selection(); + let series_id = self.app.data.sonarr_data.series.current_selection().id; + let season_number = self + .app + .data + .sonarr_data + .seasons + .current_selection() + .season_number; + let params = SonarrReleaseDownloadBody { + guid: guid.clone(), + indexer_id: *indexer_id, + series_id: Some(series_id), + season_number: Some(season_number), + ..SonarrReleaseDownloadBody::default() + }; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DownloadRelease(params)); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeasonDetails | ActiveSonarrBlock::ManualSeasonSearch => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.season_details_modal = None; + } + ActiveSonarrBlock::SeasonHistoryDetails => { + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::SeasonHistory => { + if self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .filtered_items + .is_some() + { + self + .app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .filtered_items = None; + } else { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.season_details_modal = None; + } + } + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt + | ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt + | ActiveSonarrBlock::DeleteEpisodeFilePrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::SeasonDetails if self.key == DEFAULT_KEYBINDINGS.toggle_monitoring.key => { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::ToggleEpisodeMonitoring(None)); + + self + .app + .pop_and_push_navigation_stack(self.active_sonarr_block.into()); + } + ActiveSonarrBlock::SeasonDetails + | ActiveSonarrBlock::SeasonHistory + | ActiveSonarrBlock::ManualSeasonSearch => match self.key { + _ if self.key == DEFAULT_KEYBINDINGS.refresh.key => { + self + .app + .pop_and_push_navigation_stack(self.active_sonarr_block.into()); + } + _ if self.key == DEFAULT_KEYBINDINGS.auto_search.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeasonPrompt.into()); + } + _ => (), + }, + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt + if key == DEFAULT_KEYBINDINGS.confirm.key => + { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::TriggerAutomaticSeasonSearch(None)); + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::DeleteEpisodeFilePrompt if key == DEFAULT_KEYBINDINGS.confirm.key => { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteEpisodeFile(None)); + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt + if key == DEFAULT_KEYBINDINGS.confirm.key => + { + self.app.data.sonarr_data.prompt_confirm = true; + let SonarrRelease { + guid, indexer_id, .. + } = self + .app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_releases + .current_selection(); + let series_id = self.app.data.sonarr_data.series.current_selection().id; + let season_number = self + .app + .data + .sonarr_data + .seasons + .current_selection() + .season_number; + let params = SonarrReleaseDownloadBody { + guid: guid.clone(), + indexer_id: *indexer_id, + series_id: Some(series_id), + season_number: Some(season_number), + ..SonarrReleaseDownloadBody::default() + }; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DownloadRelease(params)); + + self.app.pop_navigation_stack(); + } + _ => (), + } + } +} + +pub(in crate::handlers::sonarr_handlers::library) fn releases_sorting_options( +) -> Vec> { + vec![ + SortOption { + name: "Source", + cmp_fn: Some(|a, b| a.protocol.cmp(&b.protocol)), + }, + SortOption { + name: "Age", + cmp_fn: Some(|a, b| a.age.cmp(&b.age)), + }, + SortOption { + name: "Rejected", + cmp_fn: Some(|a, b| a.rejected.cmp(&b.rejected)), + }, + SortOption { + name: "Title", + cmp_fn: Some(|a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }), + }, + SortOption { + name: "Indexer", + cmp_fn: Some(|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase())), + }, + SortOption { + name: "Size", + cmp_fn: Some(|a, b| a.size.cmp(&b.size)), + }, + SortOption { + name: "Peers", + cmp_fn: Some(|a, b| { + let default_number = Number::from(i64::MAX); + let seeder_a = a + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + let seeder_b = b + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + + seeder_a.cmp(&seeder_b) + }), + }, + SortOption { + name: "Language", + cmp_fn: Some(|a, b| { + let default_language_vec = vec![Language { + id: 1, + name: "_".to_owned(), + }]; + let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0]; + let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0]; + + language_a.cmp(language_b) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| a.quality.cmp(&b.quality)), + }, + ] +} diff --git a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs new file mode 100644 index 0000000..dfe2a88 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs @@ -0,0 +1,1108 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::handlers::sonarr_handlers::library::season_details_handler::{ + releases_sorting_options, SeasonDetailsHandler, + }; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, SEASON_DETAILS_BLOCKS, + }; + use crate::models::servarr_models::{Language, Quality, QualityWrapper}; + use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody}; + use crate::models::HorizontallyScrollableText; + use pretty_assertions::assert_str_eq; + use rstest::rstest; + use serde_json::Number; + use std::cmp::Ordering; + use strum::IntoEnumIterator; + + mod test_handle_delete { + use super::*; + use crate::event::Key; + use pretty_assertions::assert_eq; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_episode_prompt() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + + SeasonDetailsHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::SeasonDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::DeleteEpisodeFilePrompt.into() + ); + } + + #[test] + fn test_delete_episode_prompt_no_op_when_not_ready() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + app.is_loading = true; + + SeasonDetailsHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::SeasonDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + } + } + + mod test_handle_left_right_actions { + use super::*; + use crate::event::Key; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_left_right_prompt_toggle( + #[values( + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, + ActiveSonarrBlock::DeleteEpisodeFilePrompt + )] + active_sonarr_block: ActiveSonarrBlock, + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + + SeasonDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + SeasonDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[rstest] + #[case(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory)] + #[case( + ActiveSonarrBlock::SeasonHistory, + ActiveSonarrBlock::ManualSeasonSearch + )] + #[case( + ActiveSonarrBlock::ManualSeasonSearch, + ActiveSonarrBlock::SeasonDetails + )] + fn test_season_details_tabs_left_right_action( + #[case] left_block: ActiveSonarrBlock, + #[case] right_block: ActiveSonarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.is_loading = is_ready; + app.push_navigation_stack(right_block.into()); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_details_tabs + .index = app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_details_tabs + .tabs + .iter() + .position(|tab_route| tab_route.route == right_block.into()) + .unwrap_or_default(); + + SeasonDetailsHandler::with(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None) + .handle(); + + assert_eq!( + app.get_current_route(), + app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_details_tabs + .get_active_route() + ); + assert_eq!(app.get_current_route(), left_block.into()); + + SeasonDetailsHandler::with(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None) + .handle(); + + assert_eq!( + app.get_current_route(), + app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_details_tabs + .get_active_route() + ); + assert_eq!(app.get_current_route(), right_block.into()); + } + } + + mod test_handle_submit { + use super::*; + use crate::event::Key; + use crate::models::stateful_table::StatefulTable; + use crate::network::sonarr_network::SonarrEvent; + use pretty_assertions::assert_eq; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_season_details_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + app.data.sonarr_data = create_test_sonarr_data(); + + SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::EpisodeDetails.into() + ); + } + + #[test] + fn test_season_details_submit_no_op_on_empty_episodes_table() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episodes = StatefulTable::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + + SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + } + + #[test] + fn test_season_details_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + + SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + } + + #[test] + fn test_season_history_submit() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + + SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonHistoryDetails.into() + ); + } + + #[test] + fn test_season_history_submit_no_op_when_season_history_is_empty() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history = StatefulTable::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into()); + + SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonHistory.into() + ); + } + + #[test] + fn test_season_history_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into()); + + SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonHistory.into() + ); + } + + #[rstest] + #[case( + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, + SonarrEvent::TriggerAutomaticSeasonSearch(None) + )] + #[case( + ActiveSonarrBlock::DeleteEpisodeFilePrompt, + SonarrEvent::DeleteEpisodeFile(None) + )] + fn test_season_details_prompt_confirm_submit( + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + #[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.push_navigation_stack(prompt_block.into()); + + SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + } + + #[test] + fn test_season_details_manual_search_confirm_prompt_confirm_submit() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into()); + app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into()); + + SeasonDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::ManualSeasonSearch.into() + ); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DownloadRelease(SonarrReleaseDownloadBody { + guid: String::new(), + indexer_id: 0, + series_id: Some(0), + season_number: Some(0), + ..SonarrReleaseDownloadBody::default() + })) + ); + } + + #[rstest] + fn test_season_details_prompt_decline_submit( + #[values( + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, + ActiveSonarrBlock::DeleteEpisodeFilePrompt, + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt + )] + prompt_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + } + + #[test] + fn test_manual_season_search_submit() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into()); + + SeasonDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::ManualSeasonSearch, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into() + ); + } + + #[test] + fn test_manual_season_search_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into()); + + SeasonDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::ManualSeasonSearch, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::ManualSeasonSearch.into() + ); + } + } + + mod test_handle_esc { + use super::*; + use crate::event::Key; + use crate::models::sonarr_models::SonarrHistoryItem; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + use ratatui::widgets::TableState; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_season_history_details_block_esc() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::SeasonHistoryDetails.into()); + + SeasonDetailsHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::SeasonHistoryDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonHistory.into() + ); + } + + #[rstest] + fn test_season_details_prompts_esc( + #[values( + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, + ActiveSonarrBlock::DeleteEpisodeFilePrompt, + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt + )] + prompt_block: ActiveSonarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.is_loading = is_ready; + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + SeasonDetailsHandler::with(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + } + + #[test] + fn test_season_history_esc_resets_filter_if_one_is_set_instead_of_closing_the_window() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + let mut season_history = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(vec![SonarrHistoryItem::default()]), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; + season_history.set_items(vec![SonarrHistoryItem::default()]); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history = season_history; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into()); + + SeasonDetailsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonHistory.into() + ); + assert!(app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .filter + .is_none()); + assert!(app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .filtered_items + .is_none()); + assert!(app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .filtered_state + .is_none()); + } + + #[rstest] + fn test_season_details_tabs_esc( + #[values( + ActiveSonarrBlock::SeasonDetails, + ActiveSonarrBlock::SeasonHistory, + ActiveSonarrBlock::ManualSeasonSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(active_sonarr_block.into()); + + SeasonDetailsHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert!(app.data.sonarr_data.season_details_modal.is_none()); + } + } + + mod test_handle_key_char { + use super::*; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::network::sonarr_network::SonarrEvent; + use pretty_assertions::assert_eq; + + #[test] + fn test_toggle_monitoring_key() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + app.is_routing = false; + + SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + assert!(app.data.sonarr_data.prompt_confirm); + assert!(app.is_routing); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::ToggleEpisodeMonitoring(None)) + ); + } + + #[test] + fn test_toggle_monitoring_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + app.is_routing = false; + + SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.prompt_confirm_action.is_none()); + assert!(!app.is_routing); + } + + #[rstest] + fn test_auto_search_key( + #[values( + ActiveSonarrBlock::SeasonDetails, + ActiveSonarrBlock::SeasonHistory, + ActiveSonarrBlock::ManualSeasonSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(active_sonarr_block.into()); + + SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt.into() + ); + } + + #[rstest] + fn test_auto_search_key_no_op_when_not_ready( + #[values( + ActiveSonarrBlock::SeasonDetails, + ActiveSonarrBlock::SeasonHistory, + ActiveSonarrBlock::ManualSeasonSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + + SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + } + + #[rstest] + fn test_refresh_key( + #[values( + ActiveSonarrBlock::SeasonDetails, + ActiveSonarrBlock::SeasonHistory, + ActiveSonarrBlock::ManualSeasonSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(active_sonarr_block.into()); + app.is_routing = false; + + SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(app.is_routing); + } + + #[rstest] + fn test_refresh_key_no_op_when_not_ready( + #[values( + ActiveSonarrBlock::SeasonDetails, + ActiveSonarrBlock::SeasonHistory, + ActiveSonarrBlock::ManualSeasonSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.is_routing = false; + + SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(!app.is_routing); + } + + #[rstest] + #[case( + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, + SonarrEvent::TriggerAutomaticSeasonSearch(None) + )] + #[case( + ActiveSonarrBlock::DeleteEpisodeFilePrompt, + SonarrEvent::DeleteEpisodeFile(None) + )] + fn test_season_details_prompt_confirm_confirm_key( + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + #[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.push_navigation_stack(prompt_block.into()); + + SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + } + + #[test] + fn test_season_details_manual_search_confirm_prompt_confirm_confirm_key() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into()); + app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into()); + + SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::ManualSeasonSearch.into() + ); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DownloadRelease(SonarrReleaseDownloadBody { + guid: String::new(), + indexer_id: 0, + series_id: Some(0), + season_number: Some(0), + ..SonarrReleaseDownloadBody::default() + })) + ); + } + } + + #[test] + fn test_season_details_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block) { + assert!(SeasonDetailsHandler::accepts(active_sonarr_block)); + } else { + assert!(!SeasonDetailsHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_season_details_handler_is_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + app.is_loading = true; + + let handler = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_season_details_handler_is_not_ready_when_not_loading_and_season_details_is_none() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + + let handler = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_season_details_handler_is_not_ready_when_not_loading_and_episodes_table_is_empty() { + let mut app = App::default(); + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + + let handler = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeasonDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_season_details_handler_is_not_ready_when_not_loading_and_history_table_is_empty() { + let mut app = App::default(); + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into()); + + let handler = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeasonHistory, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_season_details_handler_is_not_ready_when_not_loading_and_releases_table_is_empty() { + let mut app = App::default(); + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into()); + + let handler = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::ManualSeasonSearch, + None, + ); + + assert!(!handler.is_ready()); + } + + #[rstest] + fn test_season_details_handler_is_ready_when_not_loading_and_season_details_modal_is_populated( + #[values( + ActiveSonarrBlock::SeasonDetails, + ActiveSonarrBlock::SeasonHistory, + ActiveSonarrBlock::ManualSeasonSearch + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(active_sonarr_block.into()); + + let handler = SeasonDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + active_sonarr_block, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_releases_sorting_options_source() { + let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = + |a, b| a.protocol.cmp(&b.protocol); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[0].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Source"); + } + + #[test] + fn test_releases_sorting_options_age() { + let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| a.age.cmp(&b.age); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[1].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Age"); + } + + #[test] + fn test_releases_sorting_options_rejected() { + let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = + |a, b| a.rejected.cmp(&b.rejected); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[2].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Rejected"); + } + + #[test] + fn test_releases_sorting_options_title() { + let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[3].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Title"); + } + + #[test] + fn test_releases_sorting_options_indexer() { + let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = + |a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase()); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[4].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Indexer"); + } + + #[test] + fn test_releases_sorting_options_size() { + let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = + |a, b| a.size.cmp(&b.size); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[5].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Size"); + } + + #[test] + fn test_releases_sorting_options_peers() { + let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| { + let default_number = Number::from(i64::MAX); + let seeder_a = a + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + let seeder_b = b + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + + seeder_a.cmp(&seeder_b) + }; + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[6].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Peers"); + } + + #[test] + fn test_releases_sorting_options_language() { + let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| { + let default_language_vec = vec![Language { + id: 1, + name: "_".to_owned(), + }]; + let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0]; + let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0]; + + language_a.cmp(language_b) + }; + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[7].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Language"); + } + + #[test] + fn test_releases_sorting_options_quality() { + let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = + |a, b| a.quality.cmp(&b.quality); + let mut expected_releases_vec = release_vec(); + expected_releases_vec.sort_by(expected_cmp_fn); + + let sort_option = releases_sorting_options()[8].clone(); + let mut sorted_releases_vec = release_vec(); + sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_releases_vec, expected_releases_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + fn release_vec() -> Vec { + let release_a = SonarrRelease { + protocol: "Protocol A".to_owned(), + age: 1, + title: HorizontallyScrollableText::from("Title A"), + indexer: "Indexer A".to_owned(), + size: 1, + rejected: true, + seeders: Some(Number::from(1)), + languages: Some(vec![Language { + id: 1, + name: "Language A".to_owned(), + }]), + quality: QualityWrapper { + quality: Quality { + name: "Quality A".to_owned(), + }, + }, + ..SonarrRelease::default() + }; + let release_b = SonarrRelease { + protocol: "Protocol B".to_owned(), + age: 2, + title: HorizontallyScrollableText::from("title B"), + indexer: "indexer B".to_owned(), + size: 2, + rejected: false, + seeders: Some(Number::from(2)), + languages: Some(vec![Language { + id: 2, + name: "Language B".to_owned(), + }]), + quality: QualityWrapper { + quality: Quality { + name: "Quality B".to_owned(), + }, + }, + ..SonarrRelease::default() + }; + let release_c = SonarrRelease { + protocol: "Protocol C".to_owned(), + age: 3, + title: HorizontallyScrollableText::from("Title C"), + indexer: "Indexer C".to_owned(), + size: 3, + rejected: false, + seeders: None, + languages: None, + quality: QualityWrapper { + quality: Quality { + name: "Quality C".to_owned(), + }, + }, + ..SonarrRelease::default() + }; + + vec![release_a, release_b, release_c] + } +} diff --git a/src/handlers/sonarr_handlers/library/series_details_handler.rs b/src/handlers/sonarr_handlers/library/series_details_handler.rs new file mode 100644 index 0000000..d3201d2 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/series_details_handler.rs @@ -0,0 +1,319 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::sonarr_handlers::history::history_sorting_options; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS, +}; +use crate::models::sonarr_models::{Season, SonarrHistoryItem}; +use crate::models::BlockSelectionState; +use crate::network::sonarr_network::SonarrEvent; + +#[cfg(test)] +#[path = "series_details_handler_tests.rs"] +mod series_details_handler_tests; + +pub(super) struct SeriesDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> SeriesDetailsHandler<'a, 'b> { + handle_table_events!(self, season, self.app.data.sonarr_data.seasons, Season); + handle_table_events!( + self, + series_history, + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .expect("Series history is undefined"), + SonarrHistoryItem + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler<'a, 'b> { + fn handle(&mut self) { + let season_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::SeriesDetails.into()) + .searching_block(ActiveSonarrBlock::SearchSeason.into()) + .search_error_block(ActiveSonarrBlock::SearchSeasonError.into()) + .search_field_fn(|season: &Season| { + season + .title + .as_ref() + .expect("Season was not populated with title in handlers") + }); + let series_history_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::SeriesHistory.into()) + .sorting_block(ActiveSonarrBlock::SeriesHistorySortPrompt.into()) + .sort_options(history_sorting_options()) + .sort_by_fn(|a: &SonarrHistoryItem, b: &SonarrHistoryItem| a.id.cmp(&b.id)) + .searching_block(ActiveSonarrBlock::SearchSeriesHistory.into()) + .search_error_block(ActiveSonarrBlock::SearchSeriesHistoryError.into()) + .search_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text) + .filtering_block(ActiveSonarrBlock::FilterSeriesHistory.into()) + .filter_error_block(ActiveSonarrBlock::FilterSeriesHistoryError.into()) + .filter_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text); + + if !self.handle_season_table_events(season_table_handling_config) + && !self.handle_series_history_table_events(series_history_table_handling_config) + { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + SERIES_DETAILS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> SeriesDetailsHandler<'a, 'b> { + SeriesDetailsHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + if self.active_sonarr_block == ActiveSonarrBlock::SeriesHistory { + !self.app.is_loading && self.app.data.sonarr_data.series_history.is_some() + } else { + !self.app.is_loading + } + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails | ActiveSonarrBlock::SeriesHistory => match self.key { + _ if self.key == DEFAULT_KEYBINDINGS.left.key => { + self.app.data.sonarr_data.series_info_tabs.previous(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .sonarr_data + .series_info_tabs + .get_active_route(), + ); + } + _ if self.key == DEFAULT_KEYBINDINGS.right.key => { + self.app.data.sonarr_data.series_info_tabs.next(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .sonarr_data + .series_info_tabs + .get_active_route(), + ); + } + _ => (), + }, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + | ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + handle_prompt_toggle(self.app, self.key) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails if !self.app.data.sonarr_data.seasons.is_empty() => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + } + ActiveSonarrBlock::SeriesHistory + if !self + .app + .data + .sonarr_data + .series_history + .as_ref() + .expect("Series history should be Some") + .is_empty() => + { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SeriesHistoryDetails.into()); + } + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::TriggerAutomaticSeriesSearch(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::UpdateAndScanSeries(None)); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + | ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + ActiveSonarrBlock::SeriesHistoryDetails => { + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::SeriesHistory => { + if self + .app + .data + .sonarr_data + .series_history + .as_ref() + .expect("Series history is not populated") + .filtered_items + .is_some() + { + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .expect("Series history is not populated") + .reset_filter(); + } else { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.reset_series_info_tabs(); + } + } + ActiveSonarrBlock::SeriesDetails => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.reset_series_info_tabs(); + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails => match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => self + .app + .pop_and_push_navigation_stack(self.active_sonarr_block.into()), + _ if key == DEFAULT_KEYBINDINGS.auto_search.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into()); + } + _ if key == DEFAULT_KEYBINDINGS.update.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into()); + } + _ if key == DEFAULT_KEYBINDINGS.edit.key => { + self.app.push_navigation_stack( + ( + ActiveSonarrBlock::EditSeriesPrompt, + Some(self.active_sonarr_block), + ) + .into(), + ); + self.app.data.sonarr_data.edit_series_modal = Some((&self.app.data.sonarr_data).into()); + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + } + _ if key == DEFAULT_KEYBINDINGS.toggle_monitoring.key => { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::ToggleSeasonMonitoring(None)); + + self + .app + .pop_and_push_navigation_stack(self.active_sonarr_block.into()); + } + _ => (), + }, + ActiveSonarrBlock::SeriesHistory => match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => self + .app + .pop_and_push_navigation_stack(self.active_sonarr_block.into()), + _ if key == DEFAULT_KEYBINDINGS.auto_search.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into()); + } + _ if key == DEFAULT_KEYBINDINGS.edit.key => { + self.app.push_navigation_stack( + ( + ActiveSonarrBlock::EditSeriesPrompt, + Some(self.active_sonarr_block), + ) + .into(), + ); + self.app.data.sonarr_data.edit_series_modal = Some((&self.app.data.sonarr_data).into()); + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + } + _ if key == DEFAULT_KEYBINDINGS.update.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into()); + } + _ => (), + }, + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::TriggerAutomaticSeriesSearch(None)); + + self.app.pop_navigation_stack(); + } + } + ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::UpdateAndScanSeries(None)); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs new file mode 100644 index 0000000..3973547 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs @@ -0,0 +1,674 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, SERIES_DETAILS_BLOCKS, + }; + use crate::models::sonarr_models::Season; + use crate::models::sonarr_models::SonarrHistoryItem; + use crate::models::stateful_table::StatefulTable; + use rstest::rstest; + use strum::IntoEnumIterator; + + mod test_handle_left_right_actions { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_left_right_prompt_toggle( + #[values( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + SeriesDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + SeriesDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[rstest] + #[case(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + #[case(ActiveSonarrBlock::SeriesHistory, ActiveSonarrBlock::SeriesDetails)] + fn test_series_details_tabs_left_right_action( + #[case] left_block: ActiveSonarrBlock, + #[case] right_block: ActiveSonarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = is_ready; + app.push_navigation_stack(right_block.into()); + app.data.sonarr_data.series_info_tabs.index = app + .data + .sonarr_data + .series_info_tabs + .tabs + .iter() + .position(|tab_route| tab_route.route == right_block.into()) + .unwrap_or_default(); + + SeriesDetailsHandler::with(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None) + .handle(); + + assert_eq!( + app.get_current_route(), + app.data.sonarr_data.series_info_tabs.get_active_route() + ); + assert_eq!(app.get_current_route(), left_block.into()); + + SeriesDetailsHandler::with(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None) + .handle(); + + assert_eq!( + app.get_current_route(), + app.data.sonarr_data.series_info_tabs.get_active_route() + ); + assert_eq!(app.get_current_route(), right_block.into()); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::extended_stateful_iterable_vec; + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_series_details_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .seasons + .set_items(extended_stateful_iterable_vec!(Season, Option)); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + } + + #[test] + fn test_series_details_submit_no_op_on_empty_seasons_table() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + } + + #[test] + fn test_series_details_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesDetails, None) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + + #[test] + fn test_series_history_submit() { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistoryDetails.into() + ); + } + + #[test] + fn test_series_history_submit_no_op_when_series_history_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.data.sonarr_data.series_history = Some(StatefulTable::default()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + } + + #[test] + fn test_series_history_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesHistory, None) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + + #[rstest] + #[case( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + SonarrEvent::TriggerAutomaticSeriesSearch(None) + )] + #[case( + ActiveSonarrBlock::UpdateAndScanSeriesPrompt, + SonarrEvent::UpdateAndScanSeries(None) + )] + fn test_series_details_prompt_confirm_submit( + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + ) { + let mut app = App::default(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + } + + #[rstest] + fn test_series_details_prompt_decline_submit( + #[values( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + )] + prompt_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + } + } + + mod test_handle_esc { + use super::*; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + use ratatui::widgets::TableState; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_series_history_details_block_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistoryDetails.into()); + + SeriesDetailsHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::SeriesHistoryDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + } + + #[rstest] + fn test_series_details_prompts_esc( + #[values( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + )] + prompt_block: ActiveSonarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::default(); + app.is_loading = is_ready; + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + SeriesDetailsHandler::with(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + } + + #[test] + fn test_series_history_esc_resets_filter_if_one_is_set_instead_of_closing_the_window() { + let mut app = App::default(); + let series_history = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(vec![SonarrHistoryItem::default()]), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + + SeriesDetailsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::SeriesHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filter + .is_none()); + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_items + .is_none()); + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_state + .is_none()); + } + } + + mod test_handle_key_char { + use super::*; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::models::servarr_data::sonarr::sonarr_data::SonarrData; + use crate::models::sonarr_models::{Series, SeriesType}; + use crate::network::sonarr_network::SonarrEvent; + use crate::test_edit_series_key; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::Number; + use strum::IntoEnumIterator; + + #[rstest] + fn test_series_details_edit_key( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_edit_series_key!( + SeriesDetailsHandler, + active_sonarr_block, + active_sonarr_block + ); + } + + #[rstest] + fn test_series_edit_key_no_op_when_not_ready( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.edit.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(app.data.sonarr_data.edit_series_modal.is_none()); + } + + #[test] + fn test_toggle_monitoring_key() { + let mut app = App::default(); + app.data.sonarr_data = create_test_sonarr_data(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.is_routing = false; + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert!(app.data.sonarr_data.prompt_confirm); + assert!(app.is_routing); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::ToggleSeasonMonitoring(None)) + ); + } + + #[test] + fn test_toggle_monitoring_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.is_routing = false; + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.toggle_monitoring.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.prompt_confirm_action.is_none()); + assert!(!app.is_routing); + } + + #[rstest] + fn test_auto_search_key( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into() + ); + } + + #[rstest] + fn test_auto_search_key_no_op_when_not_ready( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + } + + #[rstest] + fn test_update_key( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into() + ); + } + + #[rstest] + fn test_update_key_no_op_when_not_ready( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + } + + #[rstest] + fn test_refresh_key( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(active_sonarr_block.into()); + app.is_routing = false; + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(app.is_routing); + } + + #[rstest] + fn test_refresh_key_no_op_when_not_ready( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.is_routing = false; + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(!app.is_routing); + } + + #[rstest] + #[case( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + SonarrEvent::TriggerAutomaticSeriesSearch(None) + )] + #[case( + ActiveSonarrBlock::UpdateAndScanSeriesPrompt, + SonarrEvent::UpdateAndScanSeries(None) + )] + fn test_series_details_prompt_confirm_confirm_key( + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.push_navigation_stack(prompt_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + } + } + + #[test] + fn test_series_details_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if SERIES_DETAILS_BLOCKS.contains(&active_sonarr_block) { + assert!(SeriesDetailsHandler::accepts(active_sonarr_block)); + } else { + assert!(!SeriesDetailsHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_series_details_handler_is_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = true; + + let handler = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_series_details_handler_is_not_ready_when_not_loading_and_series_history_is_none() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + let handler = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_series_details_handler_ready_when_not_loading_and_series_history_is_some() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.series_history = Some(StatefulTable::default()); + + let handler = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_series_details_handler_ready_when_not_loading_for_series_details() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + let handler = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/mod.rs b/src/handlers/sonarr_handlers/mod.rs new file mode 100644 index 0000000..233a2bf --- /dev/null +++ b/src/handlers/sonarr_handlers/mod.rs @@ -0,0 +1,127 @@ +use blocklist::BlocklistHandler; +use downloads::DownloadsHandler; +use history::HistoryHandler; +use indexers::IndexersHandler; +use library::LibraryHandler; +use root_folders::RootFoldersHandler; +use system::SystemHandler; + +use crate::{ + app::{key_binding::DEFAULT_KEYBINDINGS, App}, + event::Key, + models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock, +}; + +use super::KeyEventHandler; + +mod blocklist; +mod downloads; +mod history; +mod indexers; +mod library; +mod root_folders; +mod system; + +#[cfg(test)] +#[path = "sonarr_handler_tests.rs"] +mod sonarr_handler_tests; + +#[cfg(test)] +#[path = "sonarr_handler_test_utils.rs"] +mod sonarr_handler_test_utils; + +pub(super) struct SonarrHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b> { + fn handle(&mut self) { + match self.active_sonarr_block { + _ if LibraryHandler::accepts(self.active_sonarr_block) => { + LibraryHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle(); + } + _ if DownloadsHandler::accepts(self.active_sonarr_block) => { + DownloadsHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle() + } + _ if BlocklistHandler::accepts(self.active_sonarr_block) => { + BlocklistHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle() + } + _ if HistoryHandler::accepts(self.active_sonarr_block) => { + HistoryHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle() + } + _ if RootFoldersHandler::accepts(self.active_sonarr_block) => { + RootFoldersHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle() + } + _ if IndexersHandler::accepts(self.active_sonarr_block) => { + IndexersHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle() + } + _ if SystemHandler::accepts(self.active_sonarr_block) => { + SystemHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle() + } + _ => self.handle_key_event(), + } + } + + fn accepts(_active_block: ActiveSonarrBlock) -> bool { + true + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> SonarrHandler<'a, 'b> { + SonarrHandler { + key, + app, + active_sonarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + true + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) {} + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) {} + + fn handle_char_key_event(&mut self) {} +} + +pub fn handle_change_tab_left_right_keys(app: &mut App<'_>, key: Key) { + let key_ref = key; + match key_ref { + _ if key == DEFAULT_KEYBINDINGS.left.key => { + app.data.sonarr_data.main_tabs.previous(); + app.pop_and_push_navigation_stack(app.data.sonarr_data.main_tabs.get_active_route()); + } + _ if key == DEFAULT_KEYBINDINGS.right.key => { + app.data.sonarr_data.main_tabs.next(); + app.pop_and_push_navigation_stack(app.data.sonarr_data.main_tabs.get_active_route()); + } + _ => (), + } +} diff --git a/src/handlers/sonarr_handlers/root_folders/mod.rs b/src/handlers/sonarr_handlers/root_folders/mod.rs new file mode 100644 index 0000000..6e1d850 --- /dev/null +++ b/src/handlers/sonarr_handlers/root_folders/mod.rs @@ -0,0 +1,203 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; +use crate::models::servarr_models::RootFolder; +use crate::models::HorizontallyScrollableText; +use crate::network::sonarr_network::SonarrEvent; +use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys}; + +#[cfg(test)] +#[path = "root_folders_handler_tests.rs"] +mod root_folders_handler_tests; + +pub(super) struct RootFoldersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> RootFoldersHandler<'a, 'b> { + handle_table_events!( + self, + root_folders, + self.app.data.sonarr_data.root_folders, + RootFolder + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<'a, 'b> { + fn handle(&mut self) { + let root_folders_table_handling_config = + TableHandlingConfig::new(ActiveSonarrBlock::RootFolders.into()); + + if !self.handle_root_folders_table_events(root_folders_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + ROOT_FOLDERS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> RootFoldersHandler<'a, 'b> { + RootFoldersHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.sonarr_data.root_folders.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::AddRootFolderPrompt { + self + .app + .data + .sonarr_data + .edit_root_folder + .as_mut() + .unwrap() + .scroll_home() + } + } + + fn handle_end(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::AddRootFolderPrompt { + self + .app + .data + .sonarr_data + .edit_root_folder + .as_mut() + .unwrap() + .reset_offset() + } + } + + fn handle_delete(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::RootFolders { + self + .app + .push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()) + } + } + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::RootFolders => handle_change_tab_left_right_keys(self.app, self.key), + ActiveSonarrBlock::DeleteRootFolderPrompt => handle_prompt_toggle(self.app, self.key), + ActiveSonarrBlock::AddRootFolderPrompt => { + handle_text_box_left_right_keys!( + self, + self.key, + self.app.data.sonarr_data.edit_root_folder.as_mut().unwrap() + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::DeleteRootFolderPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteRootFolder(None)); + } + + self.app.pop_navigation_stack(); + } + _ if self.active_sonarr_block == ActiveSonarrBlock::AddRootFolderPrompt + && !self + .app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .text + .is_empty() => + { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddRootFolder(None)); + self.app.data.sonarr_data.prompt_confirm = true; + self.app.should_ignore_quit_key = false; + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AddRootFolderPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.edit_root_folder = None; + self.app.data.sonarr_data.prompt_confirm = false; + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::DeleteRootFolderPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::RootFolders => match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ if key == DEFAULT_KEYBINDINGS.add.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AddRootFolderPrompt.into()); + self.app.data.sonarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); + self.app.should_ignore_quit_key = true; + } + _ => (), + }, + ActiveSonarrBlock::AddRootFolderPrompt => { + handle_text_box_keys!( + self, + key, + self.app.data.sonarr_data.edit_root_folder.as_mut().unwrap() + ) + } + ActiveSonarrBlock::DeleteRootFolderPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteRootFolder(None)); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs new file mode 100644 index 0000000..1578234 --- /dev/null +++ b/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs @@ -0,0 +1,704 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::root_folders::RootFoldersHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; + use crate::models::servarr_models::RootFolder; + use crate::models::HorizontallyScrollableText; + + mod test_handle_home_end { + use crate::models::servarr_models::RootFolder; + use pretty_assertions::assert_eq; + use std::sync::atomic::Ordering; + + use super::*; + + #[test] + fn test_add_root_folder_prompt_home_end_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddRootFolderPrompt.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.edit_root_folder = Some("Test".into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 4 + ); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_root_folder_prompt() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::RootFolders, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::DeleteRootFolderPrompt.into() + ); + } + + #[test] + fn test_delete_root_folder_prompt_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::RootFolders, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_left_right_action { + use std::sync::atomic::Ordering; + + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_root_folders_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(4); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::History.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[rstest] + fn test_root_folders_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(4); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Indexers.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + + #[rstest] + fn test_left_right_delete_root_folder_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + + RootFoldersHandler::with( + key, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + RootFoldersHandler::with( + key, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[test] + fn test_add_root_folder_prompt_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.data.sonarr_data.edit_root_folder = Some("Test".into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 1 + ); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_add_root_folder_prompt_confirm_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.edit_root_folder = Some("Test".into()); + app.data.sonarr_data.prompt_confirm = true; + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddRootFolderPrompt.into()); + + RootFoldersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::AddRootFolder(None)) + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + + #[test] + fn test_add_root_folder_prompt_confirm_submit_noop_on_empty_folder() { + let mut app = App::default(); + app.data.sonarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); + app.data.sonarr_data.prompt_confirm = false; + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddRootFolderPrompt.into()); + + RootFoldersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(app.should_ignore_quit_key); + assert!(app.data.sonarr_data.prompt_confirm_action.is_none()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddRootFolderPrompt.into() + ); + } + + #[test] + fn test_delete_root_folder_prompt_confirm_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteRootFolder(None)) + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + + #[test] + fn test_delete_root_folder_prompt_decline_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_esc { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_delete_root_folder_prompt_block_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + + RootFoldersHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[test] + fn test_add_root_folder_prompt_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddRootFolderPrompt.into()); + app.data.sonarr_data.edit_root_folder = Some("/nfs/test".into()); + app.should_ignore_quit_key = true; + + RootFoldersHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + + assert!(app.data.sonarr_data.edit_root_folder.is_none()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.should_ignore_quit_key); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + + RootFoldersHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::RootFolders, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use pretty_assertions::{assert_eq, assert_str_eq}; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + #[test] + fn test_root_folder_add() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddRootFolderPrompt.into() + ); + assert!(app.should_ignore_quit_key); + assert!(app.data.sonarr_data.edit_root_folder.is_some()); + } + + #[test] + fn test_root_folder_add_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(!app.should_ignore_quit_key); + assert!(app.data.sonarr_data.edit_root_folder.is_none()); + } + + #[test] + fn test_refresh_root_folders_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_root_folders_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(!app.should_refresh); + } + + #[test] + fn test_add_root_folder_prompt_backspace_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.edit_root_folder = Some("/nfs/test".into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.edit_root_folder.as_ref().unwrap().text, + "/nfs/tes" + ); + } + + #[test] + fn test_add_root_folder_prompt_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); + + RootFoldersHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.edit_root_folder.as_ref().unwrap().text, + "h" + ); + } + + #[test] + fn test_delete_root_folder_prompt_confirm() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteRootFolder(None)) + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + } + + #[test] + fn test_root_folders_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if ROOT_FOLDERS_BLOCKS.contains(&active_sonarr_block) { + assert!(RootFoldersHandler::accepts(active_sonarr_block)); + } else { + assert!(!RootFoldersHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_root_folders_handler_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = true; + + let handler = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_root_folders_handler_not_ready_when_root_folders_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = false; + + let handler = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_root_folders_handler_ready_when_not_loading_and_root_folders_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = false; + + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + let handler = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs new file mode 100644 index 0000000..226e2dd --- /dev/null +++ b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs @@ -0,0 +1,157 @@ +#[cfg(test)] +#[macro_use] +mod utils { + + #[macro_export] + macro_rules! test_edit_series_key { + ($handler:ident, $block:expr, $context:expr) => { + let mut app = App::default(); + let mut sonarr_data = SonarrData { + quality_profile_map: bimap::BiMap::from_iter([ + (2222, "HD - 1080p".to_owned()), + (1111, "Any".to_owned()), + ]), + language_profiles_map: bimap::BiMap::from_iter([ + (2222, "English".to_owned()), + (1111, "Any".to_owned()), + ]), + tags_map: bimap::BiMap::from_iter([(1, "test".to_owned())]), + ..create_test_sonarr_data() + }; + sonarr_data.series.set_items(vec![Series { + path: "/nfs/series/Test".to_owned().into(), + monitored: true, + season_folder: true, + quality_profile_id: 2222, + language_profile_id: 2222, + series_type: SeriesType::Anime, + tags: vec![Number::from(1)], + ..Series::default() + }]); + app.data.sonarr_data = sonarr_data; + + $handler::with(DEFAULT_KEYBINDINGS.edit.key, &mut app, $block, None).handle(); + + assert_eq!( + app.get_current_route(), + (ActiveSonarrBlock::EditSeriesPrompt, Some($context)).into() + ); + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::EditSeriesToggleMonitored + ); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .series_type_list + .items, + Vec::from_iter(SeriesType::iter()) + ); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .series_type_list + .current_selection(), + &SeriesType::Anime + ); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .items, + vec!["Any".to_owned(), "HD - 1080p".to_owned()] + ); + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "HD - 1080p" + ); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .language_profile_list + .items, + vec!["Any".to_owned(), "English".to_owned()] + ); + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .language_profile_list + .current_selection(), + "English" + ); + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .path + .text, + "/nfs/series/Test" + ); + assert_str_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .tags + .text, + "test" + ); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .monitored, + Some(true) + ); + assert_eq!( + app + .data + .sonarr_data + .edit_series_modal + .as_ref() + .unwrap() + .use_season_folders, + Some(true) + ); + assert_eq!( + app.data.sonarr_data.selected_block.blocks, + $crate::models::servarr_data::sonarr::sonarr_data::EDIT_SERIES_SELECTION_BLOCKS + ); + }; + } +} diff --git a/src/handlers/sonarr_handlers/sonarr_handler_tests.rs b/src/handlers/sonarr_handlers/sonarr_handler_tests.rs new file mode 100644 index 0000000..784ccb4 --- /dev/null +++ b/src/handlers/sonarr_handlers/sonarr_handler_tests.rs @@ -0,0 +1,225 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; + use crate::handlers::sonarr_handlers::SonarrHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; + use crate::test_handler_delegation; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + #[case(0, ActiveSonarrBlock::System, ActiveSonarrBlock::Downloads)] + #[case(1, ActiveSonarrBlock::Series, ActiveSonarrBlock::Blocklist)] + #[case(2, ActiveSonarrBlock::Downloads, ActiveSonarrBlock::History)] + #[case(3, ActiveSonarrBlock::Blocklist, ActiveSonarrBlock::RootFolders)] + #[case(4, ActiveSonarrBlock::History, ActiveSonarrBlock::Indexers)] + #[case(5, ActiveSonarrBlock::RootFolders, ActiveSonarrBlock::System)] + #[case(6, ActiveSonarrBlock::Indexers, ActiveSonarrBlock::Series)] + fn test_sonarr_handler_change_tab_left_right_keys( + #[case] index: usize, + #[case] left_block: ActiveSonarrBlock, + #[case] right_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data.main_tabs.set_index(index); + + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.left.key); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + left_block.into() + ); + assert_eq!(app.get_current_route(), left_block.into()); + + app.data.sonarr_data.main_tabs.set_index(index); + + handle_change_tab_left_right_keys(&mut app, DEFAULT_KEYBINDINGS.right.key); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + right_block.into() + ); + assert_eq!(app.get_current_route(), right_block.into()); + } + + #[rstest] + fn test_delegates_library_blocks_to_library_handler( + #[values( + ActiveSonarrBlock::AddSeriesAlreadyInLibrary, + ActiveSonarrBlock::AddSeriesEmptySearchResults, + ActiveSonarrBlock::AddSeriesPrompt, + ActiveSonarrBlock::AddSeriesSearchInput, + ActiveSonarrBlock::AddSeriesSearchResults, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + ActiveSonarrBlock::AddSeriesSelectMonitor, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + ActiveSonarrBlock::AddSeriesSelectRootFolder, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + ActiveSonarrBlock::AddSeriesTagsInput, + // ActiveSonarrBlock::AutomaticallySearchEpisodePrompt, + // ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, + // ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + // ActiveSonarrBlock::DeleteEpisodeFilePrompt, + ActiveSonarrBlock::DeleteSeriesPrompt, + ActiveSonarrBlock::EditSeriesPrompt, + ActiveSonarrBlock::EditSeriesPathInput, + ActiveSonarrBlock::EditSeriesSelectSeriesType, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile, + ActiveSonarrBlock::EditSeriesTagsInput, + // ActiveSonarrBlock::EpisodeDetails, + // ActiveSonarrBlock::EpisodeFile, + // ActiveSonarrBlock::EpisodeHistory, + // ActiveSonarrBlock::EpisodesSortPrompt, + // ActiveSonarrBlock::FilterEpisodes, + // ActiveSonarrBlock::FilterEpisodesError, + ActiveSonarrBlock::FilterSeries, + ActiveSonarrBlock::FilterSeriesError, + // ActiveSonarrBlock::FilterSeriesHistory, + // ActiveSonarrBlock::FilterSeriesHistoryError, + // ActiveSonarrBlock::ManualEpisodeSearch, + // ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt, + // ActiveSonarrBlock::ManualEpisodeSearchSortPrompt, + // ActiveSonarrBlock::ManualSeasonSearch, + // ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, + // ActiveSonarrBlock::ManualSeasonSearchSortPrompt, + // ActiveSonarrBlock::SearchEpisodes, + // ActiveSonarrBlock::SearchEpisodesError, + // ActiveSonarrBlock::SearchSeason, + // ActiveSonarrBlock::SearchSeasonError, + ActiveSonarrBlock::SearchSeries, + ActiveSonarrBlock::SearchSeriesError, + // ActiveSonarrBlock::SearchSeriesHistory, + // ActiveSonarrBlock::SearchSeriesHistoryError, + // ActiveSonarrBlock::SeasonDetails, + ActiveSonarrBlock::Series, + // ActiveSonarrBlock::SeriesDetails, + // ActiveSonarrBlock::SeriesHistory, + // ActiveSonarrBlock::SeriesHistorySortPrompt, + ActiveSonarrBlock::SeriesSortPrompt, + ActiveSonarrBlock::UpdateAllSeriesPrompt, + // ActiveSonarrBlock::UpdateAndScanSeriesPrompt + // ActiveSonarrBlock::SeriesHistoryDetails, + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SonarrHandler, + ActiveSonarrBlock::Series, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_downloads_blocks_to_downloads_handler( + #[values( + ActiveSonarrBlock::Downloads, + ActiveSonarrBlock::DeleteDownloadPrompt, + ActiveSonarrBlock::UpdateDownloadsPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SonarrHandler, + ActiveSonarrBlock::Downloads, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_blocklist_blocks_to_blocklist_handler( + #[values( + ActiveSonarrBlock::Blocklist, + ActiveSonarrBlock::BlocklistItemDetails, + ActiveSonarrBlock::DeleteBlocklistItemPrompt, + ActiveSonarrBlock::BlocklistClearAllItemsPrompt, + ActiveSonarrBlock::BlocklistSortPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SonarrHandler, + ActiveSonarrBlock::Blocklist, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_history_blocks_to_history_handler( + #[values( + ActiveSonarrBlock::History, + ActiveSonarrBlock::HistoryItemDetails, + ActiveSonarrBlock::HistorySortPrompt, + ActiveSonarrBlock::FilterHistory, + ActiveSonarrBlock::FilterHistoryError, + ActiveSonarrBlock::SearchHistory, + ActiveSonarrBlock::SearchHistoryError + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SonarrHandler, + ActiveSonarrBlock::History, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_root_folders_blocks_to_root_folders_handler( + #[values( + ActiveSonarrBlock::RootFolders, + ActiveSonarrBlock::AddRootFolderPrompt, + ActiveSonarrBlock::DeleteRootFolderPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SonarrHandler, + ActiveSonarrBlock::RootFolders, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_indexers_blocks_to_indexers_handler( + #[values( + ActiveSonarrBlock::DeleteIndexerPrompt, + ActiveSonarrBlock::Indexers, + ActiveSonarrBlock::AllIndexerSettingsPrompt, + ActiveSonarrBlock::IndexerSettingsConfirmPrompt, + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput, + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput, + ActiveSonarrBlock::IndexerSettingsRetentionInput, + ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SonarrHandler, + ActiveSonarrBlock::Indexers, + active_sonarr_block + ); + } + + #[rstest] + fn test_delegates_system_blocks_to_system_handler( + #[values( + ActiveSonarrBlock::System, + ActiveSonarrBlock::SystemLogs, + ActiveSonarrBlock::SystemQueuedEvents, + ActiveSonarrBlock::SystemTasks, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt, + ActiveSonarrBlock::SystemUpdates + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SonarrHandler, + ActiveSonarrBlock::System, + active_sonarr_block + ); + } +} diff --git a/src/handlers/sonarr_handlers/system/mod.rs b/src/handlers/sonarr_handlers/system/mod.rs new file mode 100644 index 0000000..83e939a --- /dev/null +++ b/src/handlers/sonarr_handlers/system/mod.rs @@ -0,0 +1,123 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::sonarr_handlers::system::system_details_handler::SystemDetailsHandler; +use crate::handlers::{handle_clear_errors, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; +use crate::models::Scrollable; + +mod system_details_handler; + +#[cfg(test)] +#[path = "system_handler_tests.rs"] +mod system_handler_tests; + +pub(super) struct SystemHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemHandler<'a, 'b> { + fn handle(&mut self) { + match self.active_sonarr_block { + _ if SystemDetailsHandler::accepts(self.active_sonarr_block) => { + SystemDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle() + } + _ => self.handle_key_event(), + } + } + + fn accepts(active_block: ActiveSonarrBlock) -> bool { + SystemDetailsHandler::accepts(active_block) || active_block == ActiveSonarrBlock::System + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> SystemHandler<'a, 'b> { + SystemHandler { + key, + app, + active_sonarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + && !self.app.data.sonarr_data.logs.is_empty() + && !self.app.data.sonarr_data.queued_events.is_empty() + && !self.app.data.sonarr_data.tasks.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::System { + handle_change_tab_left_right_keys(self.app, self.key); + } + } + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) { + handle_clear_errors(self.app) + } + + fn handle_char_key_event(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::System { + let key = self.key; + match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ if key == DEFAULT_KEYBINDINGS.events.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SystemQueuedEvents.into()); + } + _ if key == DEFAULT_KEYBINDINGS.logs.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SystemLogs.into()); + self + .app + .data + .sonarr_data + .log_details + .set_items(self.app.data.sonarr_data.logs.items.to_vec()); + self.app.data.sonarr_data.log_details.scroll_to_bottom(); + } + _ if key == DEFAULT_KEYBINDINGS.tasks.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); + } + _ if key == DEFAULT_KEYBINDINGS.update.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SystemUpdates.into()); + } + _ => (), + } + } + } +} diff --git a/src/handlers/sonarr_handlers/system/system_details_handler.rs b/src/handlers/sonarr_handlers/system/system_details_handler.rs new file mode 100644 index 0000000..9df82b6 --- /dev/null +++ b/src/handlers/sonarr_handlers/system/system_details_handler.rs @@ -0,0 +1,181 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS}; +use crate::models::stateful_list::StatefulList; +use crate::models::Scrollable; +use crate::network::sonarr_network::SonarrEvent; + +#[cfg(test)] +#[path = "system_details_handler_tests.rs"] +mod system_details_handler_tests; + +pub(super) struct SystemDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SystemDetailsHandler<'a, 'b> { + fn accepts(active_block: ActiveSonarrBlock) -> bool { + SYSTEM_DETAILS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> SystemDetailsHandler<'a, 'b> { + SystemDetailsHandler { + key, + app, + active_sonarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + && (!self.app.data.sonarr_data.log_details.is_empty() + || !self.app.data.sonarr_data.tasks.is_empty() + || !self.app.data.sonarr_data.updates.is_empty()) + } + + fn handle_scroll_up(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SystemLogs => self.app.data.sonarr_data.log_details.scroll_up(), + ActiveSonarrBlock::SystemTasks => self.app.data.sonarr_data.tasks.scroll_up(), + ActiveSonarrBlock::SystemUpdates => self.app.data.sonarr_data.updates.scroll_up(), + ActiveSonarrBlock::SystemQueuedEvents => self.app.data.sonarr_data.queued_events.scroll_up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SystemLogs => self.app.data.sonarr_data.log_details.scroll_down(), + ActiveSonarrBlock::SystemTasks => self.app.data.sonarr_data.tasks.scroll_down(), + ActiveSonarrBlock::SystemUpdates => self.app.data.sonarr_data.updates.scroll_down(), + ActiveSonarrBlock::SystemQueuedEvents => { + self.app.data.sonarr_data.queued_events.scroll_down() + } + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SystemLogs => self.app.data.sonarr_data.log_details.scroll_to_top(), + ActiveSonarrBlock::SystemTasks => self.app.data.sonarr_data.tasks.scroll_to_top(), + ActiveSonarrBlock::SystemUpdates => self.app.data.sonarr_data.updates.scroll_to_top(), + ActiveSonarrBlock::SystemQueuedEvents => { + self.app.data.sonarr_data.queued_events.scroll_to_top() + } + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SystemLogs => self.app.data.sonarr_data.log_details.scroll_to_bottom(), + ActiveSonarrBlock::SystemTasks => self.app.data.sonarr_data.tasks.scroll_to_bottom(), + ActiveSonarrBlock::SystemUpdates => self.app.data.sonarr_data.updates.scroll_to_bottom(), + ActiveSonarrBlock::SystemQueuedEvents => { + self.app.data.sonarr_data.queued_events.scroll_to_bottom() + } + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + let key = self.key; + + match self.active_sonarr_block { + ActiveSonarrBlock::SystemLogs => match self.key { + _ if key == DEFAULT_KEYBINDINGS.left.key => { + self + .app + .data + .sonarr_data + .log_details + .items + .iter() + .for_each(|log| log.scroll_right()); + } + _ if key == DEFAULT_KEYBINDINGS.right.key => { + self + .app + .data + .sonarr_data + .log_details + .items + .iter() + .for_each(|log| log.scroll_left()); + } + _ => (), + }, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt => handle_prompt_toggle(self.app, self.key), + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SystemTasks => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SystemTaskStartConfirmPrompt.into()); + } + ActiveSonarrBlock::SystemTaskStartConfirmPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::StartTask(None)); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SystemLogs => { + self.app.data.sonarr_data.log_details = StatefulList::default(); + self.app.pop_navigation_stack() + } + ActiveSonarrBlock::SystemQueuedEvents + | ActiveSonarrBlock::SystemTasks + | ActiveSonarrBlock::SystemUpdates => self.app.pop_navigation_stack(), + ActiveSonarrBlock::SystemTaskStartConfirmPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + if SYSTEM_DETAILS_BLOCKS.contains(&self.active_sonarr_block) + && self.key == DEFAULT_KEYBINDINGS.refresh.key + { + self.app.should_refresh = true; + } + + if self.active_sonarr_block == ActiveSonarrBlock::SystemTaskStartConfirmPrompt + && self.key == DEFAULT_KEYBINDINGS.confirm.key + { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::StartTask(None)); + self.app.pop_navigation_stack(); + } + } +} diff --git a/src/handlers/sonarr_handlers/system/system_details_handler_tests.rs b/src/handlers/sonarr_handlers/system/system_details_handler_tests.rs new file mode 100644 index 0000000..94ad27f --- /dev/null +++ b/src/handlers/sonarr_handlers/system/system_details_handler_tests.rs @@ -0,0 +1,1041 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_str_eq; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::system::system_details_handler::SystemDetailsHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::models::servarr_models::QueueEvent; + use crate::models::sonarr_models::SonarrTask; + use crate::models::{HorizontallyScrollableText, ScrollableText}; + + mod test_handle_scroll_up_and_down { + use rstest::rstest; + + use crate::models::{HorizontallyScrollableText, ScrollableText}; + use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + + use super::*; + + test_iterable_scroll!( + test_log_details_scroll, + SystemDetailsHandler, + sonarr_data, + log_details, + simple_stateful_iterable_vec!(HorizontallyScrollableText, String, text), + ActiveSonarrBlock::SystemLogs, + None, + text + ); + + #[rstest] + fn test_log_details_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + app + .data + .sonarr_data + .log_details + .set_items(simple_stateful_iterable_vec!( + HorizontallyScrollableText, + String, + text + )); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemLogs, None).handle(); + + assert_str_eq!( + app.data.sonarr_data.log_details.current_selection().text, + "Test 1" + ); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemLogs, None).handle(); + + assert_str_eq!( + app.data.sonarr_data.log_details.current_selection().text, + "Test 1" + ); + } + + #[rstest] + fn test_tasks_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .sonarr_data + .tasks + .set_items(simple_stateful_iterable_vec!(SonarrTask, String, name)); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.sonarr_data.tasks.current_selection().name, + "Test 2" + ); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.sonarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[rstest] + fn test_tasks_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + app.data.sonarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .sonarr_data + .tasks + .set_items(simple_stateful_iterable_vec!(SonarrTask, String, name)); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.sonarr_data.tasks.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemTasks, None).handle(); + + assert_str_eq!( + app.data.sonarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[rstest] + fn test_queued_events_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .sonarr_data + .queued_events + .set_items(simple_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.queued_events.current_selection().name, + "Test 2" + ); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[rstest] + fn test_queued_events_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + app.data.sonarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + app + .data + .sonarr_data + .queued_events + .set_items(simple_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.queued_events.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SystemQueuedEvents, None) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_system_updates_scroll() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.updates.offset, 0); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.updates.offset, 1); + } + + #[test] + fn test_system_updates_scroll_no_op_when_not_ready() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + app.data.sonarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.up.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.updates.offset, 0); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.updates.offset, 0); + } + } + + mod test_handle_home_end { + use crate::models::{HorizontallyScrollableText, ScrollableText}; + use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + + use super::*; + + test_iterable_home_and_end!( + test_log_details_home_end, + SystemDetailsHandler, + sonarr_data, + log_details, + extended_stateful_iterable_vec!(HorizontallyScrollableText, String, text), + ActiveSonarrBlock::SystemLogs, + None, + text + ); + + #[test] + fn test_log_details_home_end_no_op_when_not_ready() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + app + .data + .sonarr_data + .log_details + .set_items(extended_stateful_iterable_vec!( + HorizontallyScrollableText, + String, + text + )); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SystemLogs, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.log_details.current_selection().text, + "Test 1" + ); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SystemLogs, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.log_details.current_selection().text, + "Test 1" + ); + } + + #[test] + fn test_tasks_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .sonarr_data + .tasks + .set_items(extended_stateful_iterable_vec!(SonarrTask, String, name)); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.tasks.current_selection().name, + "Test 3" + ); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_tasks_home_end_no_op_when_not_ready() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + app.data.sonarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .sonarr_data + .tasks + .set_items(extended_stateful_iterable_vec!(SonarrTask, String, name)); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.tasks.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SystemTasks, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.tasks.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_queued_events_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .sonarr_data + .queued_events + .set_items(extended_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.queued_events.current_selection().name, + "Test 3" + ); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_queued_events_home_end_no_op_when_not_ready() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + app.data.sonarr_data.updates = + ScrollableText::with_string("Test 1\nTest 2\nTest 3".to_owned()); + app + .data + .sonarr_data + .queued_events + .set_items(extended_stateful_iterable_vec!(QueueEvent, String, name)); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.queued_events.current_selection().name, + "Test 1" + ); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.queued_events.current_selection().name, + "Test 1" + ); + } + + #[test] + fn test_system_updates_home_end() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.updates.offset, 1); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.updates.offset, 0); + } + + #[test] + fn test_system_updates_home_end_no_op_when_not_ready() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + app.data.sonarr_data.updates = ScrollableText::with_string("Test 1\nTest 2".to_owned()); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.updates.offset, 0); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.updates.offset, 0); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[test] + fn test_handle_log_details_left_right() { + let active_sonarr_block = ActiveSonarrBlock::SystemLogs; + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app + .data + .sonarr_data + .log_details + .set_items(vec!["t1".into(), "t22".into()]); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.sonarr_data.log_details.items[1].to_string(), "t22"); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.log_details.items[0].to_string(), "1"); + assert_eq!(app.data.sonarr_data.log_details.items[1].to_string(), "22"); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.sonarr_data.log_details.items[1].to_string(), "2"); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.sonarr_data.log_details.items[1].to_string(), ""); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.log_details.items[0].to_string(), ""); + assert_eq!(app.data.sonarr_data.log_details.items[1].to_string(), ""); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.log_details.items[0].to_string(), "1"); + assert_eq!(app.data.sonarr_data.log_details.items[1].to_string(), "2"); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.sonarr_data.log_details.items[1].to_string(), "22"); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.data.sonarr_data.log_details.items[0].to_string(), "t1"); + assert_eq!(app.data.sonarr_data.log_details.items[1].to_string(), "t22"); + } + + #[rstest] + fn test_left_right_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + + SystemDetailsHandler::with( + key, + &mut app, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + SystemDetailsHandler::with( + key, + &mut app, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_system_tasks_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); + + SystemDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SystemTasks, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemTaskStartConfirmPrompt.into() + ); + } + + #[test] + fn test_system_tasks_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); + app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); + + SystemDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SystemTasks, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemTasks.into() + ); + } + + #[test] + fn test_system_tasks_start_task_prompt_confirm_submit() { + let mut app = App::default(); + app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveSonarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::StartTask(None)) + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemTasks.into() + ); + } + + #[test] + fn test_system_tasks_start_task_prompt_decline_submit() { + let mut app = App::default(); + app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveSonarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemTasks.into() + ); + } + } + + mod test_handle_esc { + use crate::models::HorizontallyScrollableText; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_esc_system_logs(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app + .data + .sonarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::from("test")]); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.push_navigation_stack(ActiveSonarrBlock::SystemLogs.into()); + app + .data + .sonarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::default()]); + + SystemDetailsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::SystemLogs, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + assert!(app.data.sonarr_data.log_details.items.is_empty()); + } + + #[rstest] + fn test_esc_system_tasks(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + SystemDetailsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::SystemTasks, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + } + + #[rstest] + fn test_esc_system_queued_events(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.push_navigation_stack(ActiveSonarrBlock::SystemQueuedEvents.into()); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + SystemDetailsHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::SystemQueuedEvents, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + } + + #[rstest] + fn test_esc_system_updates(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.push_navigation_stack(ActiveSonarrBlock::SystemUpdates.into()); + + SystemDetailsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::SystemUpdates, None) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + } + + #[test] + fn test_system_tasks_start_task_prompt_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveSonarrBlock::SystemTaskStartConfirmPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + + SystemDetailsHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemTasks.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + } + } + + mod test_handle_key_char { + use rstest::rstest; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + #[rstest] + fn test_refresh_key( + #[values( + ActiveSonarrBlock::SystemLogs, + ActiveSonarrBlock::SystemTasks, + ActiveSonarrBlock::SystemQueuedEvents, + ActiveSonarrBlock::SystemUpdates + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(active_sonarr_block.into()); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(app.should_refresh); + } + + #[rstest] + fn test_refresh_key_no_op_when_not_ready( + #[values( + ActiveSonarrBlock::SystemLogs, + ActiveSonarrBlock::SystemTasks, + ActiveSonarrBlock::SystemQueuedEvents, + ActiveSonarrBlock::SystemUpdates + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(active_sonarr_block.into()); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_system_tasks_start_task_prompt_confirm() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); + app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); + app.push_navigation_stack(ActiveSonarrBlock::SystemTaskStartConfirmPrompt.into()); + + SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::StartTask(None)) + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemTasks.into() + ); + } + } + + #[test] + fn test_system_details_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if SYSTEM_DETAILS_BLOCKS.contains(&active_sonarr_block) { + assert!(SystemDetailsHandler::accepts(active_sonarr_block)); + } else { + assert!(!SystemDetailsHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_system_details_handler_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + + let handler = SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_system_details_handler_not_ready_when_log_details_and_updates_and_tasks_are_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = false; + + let handler = SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_log_details_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = false; + app + .data + .sonarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::default()]); + + let handler = SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_tasks_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = false; + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + let handler = SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SystemTasks, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_system_details_handler_ready_when_not_loading_and_updates_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = false; + app.data.sonarr_data.updates = ScrollableText::with_string("Test".to_owned()); + + let handler = SystemDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SystemUpdates, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/system/system_handler_tests.rs b/src/handlers/sonarr_handlers/system/system_handler_tests.rs new file mode 100644 index 0000000..6843b5e --- /dev/null +++ b/src/handlers/sonarr_handlers/system/system_handler_tests.rs @@ -0,0 +1,571 @@ +#[cfg(test)] +mod tests { + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::system::SystemHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::models::servarr_models::QueueEvent; + use crate::models::sonarr_models::SonarrTask; + use crate::test_handler_delegation; + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + + use super::*; + + #[rstest] + fn test_system_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(6); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Indexers.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + + #[rstest] + fn test_system_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(6); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Series.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_default_esc(#[values(true, false)] is_loading: bool) { + let mut app = App::default(); + app.is_loading = is_loading; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + + SystemHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::System, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use pretty_assertions::{assert_eq, assert_str_eq}; + + use crate::models::HorizontallyScrollableText; + + use super::*; + + #[test] + fn test_update_system_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemUpdates.into() + ); + } + + #[test] + fn test_update_system_key_no_op_if_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + } + + #[test] + fn test_queued_events_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.events.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemQueuedEvents.into() + ); + } + + #[test] + fn test_queued_events_key_no_op_if_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.events.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + } + + #[test] + fn test_refresh_system_key() { + let mut app = App::default(); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_system_key_no_op_if_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_logs_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.logs.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemLogs.into() + ); + assert_eq!( + app.data.sonarr_data.log_details.items, + app.data.sonarr_data.logs.items + ); + assert_str_eq!( + app.data.sonarr_data.log_details.current_selection().text, + "test 2" + ); + } + + #[test] + fn test_logs_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.logs.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + assert!(app.data.sonarr_data.log_details.is_empty()); + } + + #[test] + fn test_tasks_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.tasks.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SystemTasks.into() + ); + } + + #[test] + fn test_tasks_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.data.sonarr_data.logs.set_items(vec![ + HorizontallyScrollableText::from("test 1"), + HorizontallyScrollableText::from("test 2"), + ]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + SystemHandler::with( + DEFAULT_KEYBINDINGS.tasks.key, + &mut app, + ActiveSonarrBlock::System, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::System.into()); + } + } + + #[rstest] + fn test_delegates_system_details_blocks_to_system_details_handler( + #[values( + ActiveSonarrBlock::SystemLogs, + ActiveSonarrBlock::SystemQueuedEvents, + ActiveSonarrBlock::SystemTasks, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt, + ActiveSonarrBlock::SystemUpdates + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SystemHandler, + ActiveSonarrBlock::System, + active_sonarr_block + ); + } + + #[test] + fn test_system_handler_accepts() { + let mut system_blocks = vec![ActiveSonarrBlock::System]; + system_blocks.extend(SYSTEM_DETAILS_BLOCKS); + + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if system_blocks.contains(&active_sonarr_block) { + assert!(SystemHandler::accepts(active_sonarr_block)); + } else { + assert!(!SystemHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_system_handler_is_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = true; + + let system_handler = SystemHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_not_ready_when_logs_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = false; + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + let system_handler = SystemHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_not_ready_when_tasks_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = false; + app.data.sonarr_data.logs.set_items(vec!["test".into()]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + let system_handler = SystemHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_not_ready_when_queued_events_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = false; + app.data.sonarr_data.logs.set_items(vec!["test".into()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + + let system_handler = SystemHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::System, + None, + ); + + assert!(!system_handler.is_ready()); + } + + #[test] + fn test_system_handler_is_ready_when_all_required_tables_are_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::System.into()); + app.is_loading = false; + app.data.sonarr_data.logs.set_items(vec!["test".into()]); + app + .data + .sonarr_data + .tasks + .set_items(vec![SonarrTask::default()]); + app + .data + .sonarr_data + .queued_events + .set_items(vec![QueueEvent::default()]); + + let system_handler = SystemHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveSonarrBlock::System, + None, + ); + + assert!(system_handler.is_ready()); + } +} diff --git a/src/handlers/table_handler.rs b/src/handlers/table_handler.rs new file mode 100644 index 0000000..56b77b1 --- /dev/null +++ b/src/handlers/table_handler.rs @@ -0,0 +1,417 @@ +use crate::models::stateful_table::SortOption; +use crate::models::Route; +use derive_setters::Setters; +use std::cmp::Ordering; +use std::fmt::Debug; + +#[cfg(test)] +#[path = "table_handler_tests.rs"] +mod table_handler_tests; + +#[derive(Setters)] +pub struct TableHandlingConfig +where + T: Clone + PartialEq + Eq + Debug + Default, +{ + #[setters(strip_option)] + pub sorting_block: Option, + #[setters(strip_option)] + pub sort_options: Option>>, + #[setters(strip_option)] + pub sort_by_fn: Option Ordering>, + #[setters(strip_option)] + pub searching_block: Option, + #[setters(strip_option)] + pub search_error_block: Option, + #[setters(strip_option)] + pub search_field_fn: Option &str>, + #[setters(strip_option)] + pub filtering_block: Option, + #[setters(strip_option)] + pub filter_error_block: Option, + #[setters(strip_option)] + pub filter_field_fn: Option &str>, + #[setters(skip)] + pub table_block: Route, +} + +#[macro_export] +macro_rules! handle_table_events { + ($self:expr, $name:ty, $table:expr, $row:ident) => { + paste::paste! { + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + if $self.is_ready() { + match $self.key { + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.up.key => $self.[](config), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.down.key => $self.[](config), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.home.key => $self.[](config), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.end.key => $self.[](config), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.left.key + || $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.right.key => + { + $self.[](config) + } + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.submit.key => $self.[](config), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.esc.key => $self.[](config), + _ if config.searching_block.is_some() + && $self.app.get_current_route() == *config.searching_block.as_ref().unwrap() => + { + $self.[]() + } + _ if config.filtering_block.is_some() + && $self.app.get_current_route() == *config.filtering_block.as_ref().unwrap() => + { + $self.[]() + } + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.filter.key + && config.filtering_block.is_some() => $self.[](config), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.search.key + && config.searching_block.is_some() => $self.[](config), + _ if $self.key == $crate::app::key_binding::DEFAULT_KEYBINDINGS.sort.key + && config.sorting_block.is_some() => $self.[](config), + _ => false, + } + } else { + false + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + use $crate::models::Scrollable; + + match $self.app.get_current_route() { + _ if config.table_block == $self.app.get_current_route() => { + $table.scroll_up(); + true + } + _ if config.sorting_block.is_some() + && $self.app.get_current_route() == *config.sorting_block.as_ref().unwrap() => + { + $table.sort.as_mut().unwrap().scroll_up(); + true + } + _ => false, + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + use $crate::models::Scrollable; + + match $self.app.get_current_route() { + _ if config.table_block == $self.app.get_current_route() => { + $table.scroll_down(); + true + } + _ if config.sorting_block.is_some() + && $self.app.get_current_route() == *config.sorting_block.as_ref().unwrap() => + { + $table + .sort + .as_mut() + .unwrap() + .scroll_down(); + true + } + _ => false, + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + use $crate::models::Scrollable; + + match $self.app.get_current_route() { + _ if config.table_block == $self.app.get_current_route() => { + $table.scroll_to_top(); + true + } + _ if config.sorting_block.is_some() + && $self.app.get_current_route() == *config.sorting_block.as_ref().unwrap() => + { + $table + .sort + .as_mut() + .unwrap() + .scroll_to_top(); + true + } + _ if config.searching_block.is_some() + && $self.app.get_current_route() == *config.searching_block.as_ref().unwrap() => + { + $table + .search + .as_mut() + .unwrap() + .scroll_home(); + true + } + _ if config.filtering_block.is_some() + && $self.app.get_current_route() == *config.filtering_block.as_ref().unwrap() => + { + $table + .filter + .as_mut() + .unwrap() + .scroll_home(); + true + } + _ => false, + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + use $crate::models::Scrollable; + + match $self.app.get_current_route() { + _ if config.table_block == $self.app.get_current_route() => { + $table.scroll_to_bottom(); + true + } + _ if config.sorting_block.is_some() + && $self.app.get_current_route() == *config.sorting_block.as_ref().unwrap() => + { + $table + .sort + .as_mut() + .unwrap() + .scroll_to_bottom(); + true + } + _ if config.searching_block.is_some() + && $self.app.get_current_route() == *config.searching_block.as_ref().unwrap() => + { + $table + .search + .as_mut() + .unwrap() + .reset_offset(); + true + } + _ if config.filtering_block.is_some() + && $self.app.get_current_route() == *config.filtering_block.as_ref().unwrap() => + { + $table + .filter + .as_mut() + .unwrap() + .reset_offset(); + true + } + _ => false, + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + match $self.app.get_current_route() { + _ if config.searching_block.is_some() + && $self.app.get_current_route() == *config.searching_block.as_ref().unwrap() => + { + $crate::handle_text_box_left_right_keys!( + $self, + $self.key, + $table.search.as_mut().unwrap() + ); + true + } + _ if config.filtering_block.is_some() + && $self.app.get_current_route() == *config.filtering_block.as_ref().unwrap() => + { + $crate::handle_text_box_left_right_keys!( + $self, + $self.key, + $table.filter.as_mut().unwrap() + ); + true + } + _ => false, + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + match $self.app.get_current_route() { + _ if config.sorting_block.is_some() + && $self.app.get_current_route() == *config.sorting_block.as_ref().unwrap() => + { + if let Some(sort_by_fn) = config.sort_by_fn { + $table.items.sort_by(sort_by_fn); + } + + $table.apply_sorting(); + $self.app.pop_navigation_stack(); + + true + } + _ if config.searching_block.is_some() + && $self.app.get_current_route() == *config.searching_block.as_ref().unwrap() => + { + $self.app.pop_navigation_stack(); + $self.app.should_ignore_quit_key = false; + + if $table.search.is_some() { + let search_field_fn = config + .search_field_fn + .expect("Search field function is required"); + let has_match = $table.apply_search(search_field_fn); + + if !has_match { + $self.app.push_navigation_stack( + config + .search_error_block + .expect("Search error block is undefined"), + ); + } + } + + true + } + _ if config.filtering_block.is_some() + && $self.app.get_current_route() == *config.filtering_block.as_ref().unwrap() => + { + $self.app.pop_navigation_stack(); + $self.app.should_ignore_quit_key = false; + + if $table.filter.is_some() { + let filter_field_fn = config + .filter_field_fn + .expect("Search field function is required"); + let has_match = $table.apply_filter(filter_field_fn); + + if !has_match { + $self.app.push_navigation_stack( + config + .filter_error_block + .expect("Search error block is undefined"), + ); + } + } + + true + } + _ => false, + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + match $self.app.get_current_route() { + _ if config.sorting_block.is_some() + && $self.app.get_current_route() == *config.sorting_block.as_ref().unwrap() => + { + $self.app.pop_navigation_stack(); + true + } + _ if (config.searching_block.is_some() + && $self.app.get_current_route() == *config.searching_block.as_ref().unwrap()) + || (config.search_error_block.is_some() + && $self.app.get_current_route() == *config.search_error_block.as_ref().unwrap()) => + { + $self.app.pop_navigation_stack(); + $table.reset_search(); + $self.app.should_ignore_quit_key = false; + true + } + _ if (config.filtering_block.is_some() + && $self.app.get_current_route() == *config.filtering_block.as_ref().unwrap()) + || (config.filter_error_block.is_some() + && $self.app.get_current_route() == *config.filter_error_block.as_ref().unwrap()) => + { + $self.app.pop_navigation_stack(); + $table.reset_filter(); + $self.app.should_ignore_quit_key = false; + true + } + _ if config.table_block == $self.app.get_current_route() + && $table.filtered_items.is_some() => + { + $table.reset_filter(); + true + } + _ => false, + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + if matches!($self.app.get_current_route(), _ if config.table_block == $self.app.get_current_route()) { + $self + .app + .push_navigation_stack(config.filtering_block.expect("Filtering block is undefined").into()); + $table.reset_filter(); + $table.filter = Some($crate::models::HorizontallyScrollableText::default()); + $self.app.should_ignore_quit_key = true; + + true + } else { + false + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + if matches!($self.app.get_current_route(), _ if config.table_block == $self.app.get_current_route()) { + $self + .app + .push_navigation_stack(config.searching_block.expect("Searching block is undefined")); + $table.search = Some($crate::models::HorizontallyScrollableText::default()); + $self.app.should_ignore_quit_key = true; + + true + } else { + false + } + } + + fn [](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool { + if matches!($self.app.get_current_route(), _ if config.table_block == $self.app.get_current_route()) { + $table.sorting( + config + .sort_options + .as_ref() + .expect("Sort options are undefined") + .clone(), + ); + $self + .app + .push_navigation_stack(config.sorting_block.expect("Sorting block is undefined")); + true + } else { + false + } + } + + fn [](&mut $self) -> bool { + $crate::handle_text_box_keys!( + $self, + $self.key, + $table.search.as_mut().unwrap() + ); + true + } + + fn [](&mut $self) -> bool { + $crate::handle_text_box_keys!( + $self, + $self.key, + $table.filter.as_mut().unwrap() + ); + true + } + } + }; +} + +impl TableHandlingConfig +where + T: Clone + PartialEq + Eq + Debug + Default, +{ + pub fn new(table_block: Route) -> Self { + TableHandlingConfig { + sorting_block: None, + sort_options: None, + sort_by_fn: None, + searching_block: None, + search_error_block: None, + search_field_fn: None, + filtering_block: None, + filter_error_block: None, + filter_field_fn: None, + table_block, + } + } +} diff --git a/src/handlers/table_handler_tests.rs b/src/handlers/table_handler_tests.rs new file mode 100644 index 0000000..2861ce8 --- /dev/null +++ b/src/handlers/table_handler_tests.rs @@ -0,0 +1,1221 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handle_table_events; + use crate::handlers::table_handler::TableHandlingConfig; + use crate::handlers::KeyEventHandler; + use crate::models::radarr_models::Movie; + use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::models::servarr_models::Language; + use crate::models::stateful_table::SortOption; + use rstest::rstest; + + struct TableHandlerUnit<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_radarr_block: ActiveRadarrBlock, + _context: Option, + } + + impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TableHandlerUnit<'a, 'b> { + fn handle(&mut self) { + let movie_table_handling_config = TableHandlingConfig::new(ActiveRadarrBlock::Movies.into()) + .sorting_block(ActiveRadarrBlock::MoviesSortPrompt.into()) + .sort_by_fn(|a: &Movie, b: &Movie| a.id.cmp(&b.id)) + .sort_options(sort_options()) + .searching_block(ActiveRadarrBlock::SearchMovie.into()) + .search_error_block(ActiveRadarrBlock::SearchMovieError.into()) + .search_field_fn(|movie| &movie.title.text) + .filtering_block(ActiveRadarrBlock::FilterMovies.into()) + .filter_error_block(ActiveRadarrBlock::FilterMoviesError.into()) + .filter_field_fn(|movie| &movie.title.text); + let minimal_movie_table_handling_config = + TableHandlingConfig::new(ActiveRadarrBlock::Movies.into()); + + match self.active_radarr_block { + ActiveRadarrBlock::MovieDetails => { + self.handle_movies_table_events(minimal_movie_table_handling_config); + } + _ => { + self.handle_movies_table_events(movie_table_handling_config); + } + } + } + + fn accepts(_: ActiveRadarrBlock) -> bool { + true + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveRadarrBlock, + _context: Option, + ) -> Self { + Self { + key, + app, + active_radarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) {} + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) {} + + fn handle_char_key_event(&mut self) {} + } + + impl<'a, 'b> TableHandlerUnit<'a, 'b> { + handle_table_events!(self, movies, self.app.data.radarr_data.movies, Movie); + } + + mod test_handle_scroll_up_and_down { + use super::*; + use crate::models::HorizontallyScrollableText; + use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + use pretty_assertions::assert_str_eq; + + test_iterable_scroll!( + test_table_scroll, + TableHandlerUnit, + radarr_data, + movies, + simple_stateful_iterable_vec!(Movie, HorizontallyScrollableText), + ActiveRadarrBlock::Movies, + None, + title, + to_string + ); + + #[rstest] + fn test_table_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.is_loading = true; + app + .data + .radarr_data + .movies + .set_items(simple_stateful_iterable_vec!( + Movie, + HorizontallyScrollableText + )); + + TableHandlerUnit::with(key, &mut app, ActiveRadarrBlock::Movies, None).handle(); + + assert_str_eq!( + app + .data + .radarr_data + .movies + .current_selection() + .title + .to_string(), + "Test 1" + ); + + TableHandlerUnit::with(key, &mut app, ActiveRadarrBlock::Movies, None).handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .current_selection() + .title + .to_string(), + "Test 1" + ); + } + + #[rstest] + fn test_table_sort_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let movie_field_vec = sort_options(); + let mut app = App::default(); + app.data.radarr_data.movies.sorting(sort_options()); + + if key == Key::Up { + for i in (0..movie_field_vec.len()).rev() { + TableHandlerUnit::with(key, &mut app, ActiveRadarrBlock::MoviesSortPrompt, None).handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .sort + .as_ref() + .unwrap() + .current_selection(), + &movie_field_vec[i] + ); + } + } else { + for i in 0..movie_field_vec.len() { + TableHandlerUnit::with(key, &mut app, ActiveRadarrBlock::MoviesSortPrompt, None).handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .sort + .as_ref() + .unwrap() + .current_selection(), + &movie_field_vec[(i + 1) % movie_field_vec.len()] + ); + } + } + } + } + + mod test_handle_home_end { + use pretty_assertions::{assert_eq, assert_str_eq}; + use std::sync::atomic::Ordering::SeqCst; + + use super::*; + use crate::models::HorizontallyScrollableText; + use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + + test_iterable_home_and_end!( + test_table_home_end, + TableHandlerUnit, + radarr_data, + movies, + extended_stateful_iterable_vec!(Movie, HorizontallyScrollableText), + ActiveRadarrBlock::Movies, + None, + title, + to_string + ); + + #[test] + fn test_table_home_end_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app + .data + .radarr_data + .movies + .set_items(extended_stateful_iterable_vec!( + Movie, + HorizontallyScrollableText + )); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveRadarrBlock::Movies, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .movies + .current_selection() + .title + .to_string(), + "Test 1" + ); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveRadarrBlock::Movies, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .movies + .current_selection() + .title + .to_string(), + "Test 1" + ); + } + + #[test] + fn test_movie_search_box_home_end_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + app.data.radarr_data.movies.search = Some("Test".into()); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveRadarrBlock::SearchMovie, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 4 + ); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveRadarrBlock::SearchMovie, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + + #[test] + fn test_movie_filter_box_home_end_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + app.data.radarr_data.movies.filter = Some("Test".into()); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveRadarrBlock::FilterMovies, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 4 + ); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveRadarrBlock::FilterMovies, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + + #[test] + fn test_table_sort_home_end() { + let movie_field_vec = sort_options(); + let mut app = App::default(); + app.data.radarr_data.movies.sorting(sort_options()); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveRadarrBlock::MoviesSortPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .sort + .as_ref() + .unwrap() + .current_selection(), + &movie_field_vec[movie_field_vec.len() - 1] + ); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveRadarrBlock::MoviesSortPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .sort + .as_ref() + .unwrap() + .current_selection(), + &movie_field_vec[0] + ); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use std::sync::atomic::Ordering::SeqCst; + + use super::*; + + #[test] + fn test_movie_search_box_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + app.data.radarr_data.movies.search = Some("Test".into()); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveRadarrBlock::SearchMovie, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 1 + ); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveRadarrBlock::SearchMovie, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + + #[test] + fn test_movie_filter_box_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + app.data.radarr_data.movies.filter = Some("Test".into()); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveRadarrBlock::FilterMovies, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 1 + ); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveRadarrBlock::FilterMovies, + None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::{assert_eq, assert_str_eq}; + + use crate::extended_stateful_iterable_vec; + use crate::models::HorizontallyScrollableText; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_search_movie_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); + app + .data + .radarr_data + .movies + .set_items(extended_stateful_iterable_vec!( + Movie, + HorizontallyScrollableText + )); + app.data.radarr_data.movies.search = Some("Test 2".into()); + + TableHandlerUnit::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::SearchMovie, None).handle(); + + assert_str_eq!( + app.data.radarr_data.movies.current_selection().title.text, + "Test 2" + ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + } + + #[test] + fn test_search_movie_submit_error_on_no_search_hits() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); + app + .data + .radarr_data + .movies + .set_items(extended_stateful_iterable_vec!( + Movie, + HorizontallyScrollableText + )); + app.data.radarr_data.movies.search = Some("Test 5".into()); + + TableHandlerUnit::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::SearchMovie, None).handle(); + + assert_str_eq!( + app.data.radarr_data.movies.current_selection().title.text, + "Test 1" + ); + assert_eq!( + app.get_current_route(), + ActiveRadarrBlock::SearchMovieError.into() + ); + } + + #[test] + fn test_search_filtered_table_submit() { + let mut app = App::default(); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); + app + .data + .radarr_data + .movies + .set_filtered_items(extended_stateful_iterable_vec!( + Movie, + HorizontallyScrollableText + )); + app.data.radarr_data.movies.search = Some("Test 2".into()); + + TableHandlerUnit::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::SearchMovie, None).handle(); + + assert_str_eq!( + app.data.radarr_data.movies.current_selection().title.text, + "Test 2" + ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + } + + #[test] + fn test_filter_table_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); + app + .data + .radarr_data + .movies + .set_items(extended_stateful_iterable_vec!( + Movie, + HorizontallyScrollableText + )); + app.data.radarr_data.movies.filter = Some("Test".into()); + + TableHandlerUnit::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::FilterMovies, None).handle(); + + assert!(app.data.radarr_data.movies.filtered_items.is_some()); + assert!(!app.should_ignore_quit_key); + assert_eq!( + app + .data + .radarr_data + .movies + .filtered_items + .as_ref() + .unwrap() + .len(), + 3 + ); + assert_str_eq!( + app.data.radarr_data.movies.current_selection().title.text, + "Test 1" + ); + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + } + + #[test] + fn test_filter_table_submit_error_on_no_filter_matches() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); + app + .data + .radarr_data + .movies + .set_items(extended_stateful_iterable_vec!( + Movie, + HorizontallyScrollableText + )); + app.data.radarr_data.movies.filter = Some("Test 5".into()); + + TableHandlerUnit::with(SUBMIT_KEY, &mut app, ActiveRadarrBlock::FilterMovies, None).handle(); + + assert!(!app.should_ignore_quit_key); + assert!(app.data.radarr_data.movies.filtered_items.is_none()); + assert_eq!( + app.get_current_route(), + ActiveRadarrBlock::FilterMoviesError.into() + ); + } + + #[test] + fn test_table_sort_prompt_submit() { + let mut app = App::default(); + app.data.radarr_data.movies.sort_asc = true; + app.data.radarr_data.movies.sorting(sort_options()); + app.data.radarr_data.movies.set_items(movies_vec()); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); + + let mut expected_vec = movies_vec(); + expected_vec.sort_by(|a, b| a.id.cmp(&b.id)); + expected_vec.reverse(); + + TableHandlerUnit::with( + SUBMIT_KEY, + &mut app, + ActiveRadarrBlock::MoviesSortPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert_eq!(app.data.radarr_data.movies.items, expected_vec); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use ratatui::widgets::TableState; + + use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; + use crate::models::stateful_table::StatefulTable; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_search_movie_block_esc( + #[values(ActiveRadarrBlock::SearchMovie, ActiveRadarrBlock::SearchMovieError)] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(active_radarr_block.into()); + app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.movies.search = Some("Test".into()); + + TableHandlerUnit::with(ESC_KEY, &mut app, active_radarr_block, None).handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.radarr_data.movies.search, None); + } + + #[rstest] + fn test_filter_table_block_esc( + #[values(ActiveRadarrBlock::FilterMovies, ActiveRadarrBlock::FilterMoviesError)] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(active_radarr_block.into()); + app.data.radarr_data = create_test_radarr_data(); + app.data.radarr_data.movies = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(Vec::new()), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with(ESC_KEY, &mut app, active_radarr_block, None).handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.radarr_data.movies.filter, None); + assert_eq!(app.data.radarr_data.movies.filtered_items, None); + assert_eq!(app.data.radarr_data.movies.filtered_state, None); + } + + #[test] + fn test_table_sort_prompt_block_esc() { + let mut app = App::default(); + app.data.radarr_data.movies.set_items(movies_vec()); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); + + TableHandlerUnit::with(ESC_KEY, &mut app, ActiveRadarrBlock::MoviesSortPrompt, None).handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + } + } + + mod test_handle_key_char { + use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; + use crate::models::HorizontallyScrollableText; + use pretty_assertions::{assert_eq, assert_str_eq}; + + use super::*; + + #[test] + fn test_search_table_key() { + let mut app = App::default(); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.search.key, + &mut app, + ActiveRadarrBlock::Movies, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveRadarrBlock::SearchMovie.into() + ); + assert!(app.should_ignore_quit_key); + assert_eq!( + app.data.radarr_data.movies.search, + Some(HorizontallyScrollableText::default()) + ); + } + + #[test] + fn test_search_table_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.search.key, + &mut app, + ActiveRadarrBlock::Movies, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.radarr_data.movies.search, None); + } + + #[test] + fn test_search_table_key_no_op_when_search_table_block_is_not_defined() { + let mut app = App::default(); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.search.key, + &mut app, + ActiveRadarrBlock::MovieDetails, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.radarr_data.movies.search, None); + } + + #[test] + fn test_filter_table_key() { + let mut app = App::default(); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveRadarrBlock::Movies, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveRadarrBlock::FilterMovies.into() + ); + assert!(app.should_ignore_quit_key); + assert!(app.data.radarr_data.movies.filter.is_some()); + } + + #[test] + fn test_filter_table_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveRadarrBlock::Movies, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert!(!app.should_ignore_quit_key); + assert!(app.data.radarr_data.movies.filter.is_none()); + } + + #[test] + fn test_filter_table_key_resets_previous_filter() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.data.radarr_data = create_test_radarr_data(); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + app.data.radarr_data.movies.filter = Some("Test".into()); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveRadarrBlock::Movies, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveRadarrBlock::FilterMovies.into() + ); + assert!(app.should_ignore_quit_key); + assert_eq!( + app.data.radarr_data.movies.filter, + Some(HorizontallyScrollableText::default()) + ); + assert!(app.data.radarr_data.movies.filtered_items.is_none()); + assert!(app.data.radarr_data.movies.filtered_state.is_none()); + } + + #[test] + fn test_filter_table_key_no_op_when_filter_table_block_is_not_defined() { + let mut app = App::default(); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveRadarrBlock::MovieDetails, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.radarr_data.movies.filter, None); + } + + #[test] + fn test_search_table_box_backspace_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); + app.data.radarr_data.movies.search = Some("Test".into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveRadarrBlock::SearchMovie, + None, + ) + .handle(); + + assert_str_eq!( + app.data.radarr_data.movies.search.as_ref().unwrap().text, + "Tes" + ); + } + + #[test] + fn test_filter_table_box_backspace_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + app.data.radarr_data.movies.filter = Some("Test".into()); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveRadarrBlock::FilterMovies, + None, + ) + .handle(); + + assert_str_eq!( + app.data.radarr_data.movies.filter.as_ref().unwrap().text, + "Tes" + ); + } + + #[test] + fn test_search_table_box_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + app.data.radarr_data.movies.search = Some(HorizontallyScrollableText::default()); + + TableHandlerUnit::with( + Key::Char('h'), + &mut app, + ActiveRadarrBlock::SearchMovie, + None, + ) + .handle(); + + assert_str_eq!( + app.data.radarr_data.movies.search.as_ref().unwrap().text, + "h" + ); + } + + #[test] + fn test_filter_table_box_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + app.data.radarr_data.movies.filter = Some(HorizontallyScrollableText::default()); + + TableHandlerUnit::with( + Key::Char('h'), + &mut app, + ActiveRadarrBlock::FilterMovies, + None, + ) + .handle(); + + assert_str_eq!( + app.data.radarr_data.movies.filter.as_ref().unwrap().text, + "h" + ); + } + + #[test] + fn test_sort_key() { + let mut app = App::default(); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.sort.key, + &mut app, + ActiveRadarrBlock::Movies, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveRadarrBlock::MoviesSortPrompt.into() + ); + assert_eq!( + app.data.radarr_data.movies.sort.as_ref().unwrap().items, + sort_options() + ); + assert!(!app.data.radarr_data.movies.sort_asc); + } + + #[test] + fn test_sort_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.sort.key, + &mut app, + ActiveRadarrBlock::Movies, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert!(app.data.radarr_data.movies.sort.is_none()); + } + + #[test] + fn test_sort_key_no_op_when_sort_table_block_is_undefined() { + let mut app = App::default(); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + TableHandlerUnit::with( + DEFAULT_KEYBINDINGS.sort.key, + &mut app, + ActiveRadarrBlock::MovieDetails, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert!(app.data.radarr_data.movies.sort.is_none()); + } + } + + fn movies_vec() -> Vec { + vec![ + Movie { + id: 3, + title: "test 1".into(), + original_language: Language { + id: 1, + name: "English".to_owned(), + }, + size_on_disk: 1024, + studio: "Studio 1".to_owned(), + year: 2024, + monitored: false, + runtime: 12.into(), + quality_profile_id: 1, + certification: Some("PG-13".to_owned()), + tags: vec![1.into(), 2.into()], + ..Movie::default() + }, + Movie { + id: 2, + title: "test 2".into(), + original_language: Language { + id: 2, + name: "Chinese".to_owned(), + }, + size_on_disk: 2048, + studio: "Studio 2".to_owned(), + year: 1998, + monitored: false, + runtime: 60.into(), + quality_profile_id: 2, + certification: Some("R".to_owned()), + tags: vec![1.into(), 3.into()], + ..Movie::default() + }, + Movie { + id: 1, + title: "test 3".into(), + original_language: Language { + id: 3, + name: "Japanese".to_owned(), + }, + size_on_disk: 512, + studio: "studio 3".to_owned(), + year: 1954, + monitored: true, + runtime: 120.into(), + quality_profile_id: 3, + certification: Some("G".to_owned()), + tags: vec![2.into(), 3.into()], + ..Movie::default() + }, + ] + } + + fn sort_options() -> Vec> { + vec![SortOption { + name: "Test 1", + cmp_fn: Some(|a, b| { + b.title + .text + .to_lowercase() + .cmp(&a.title.text.to_lowercase()) + }), + }] + } +} diff --git a/src/logos.rs b/src/logos.rs index d7ae5db..f0c6815 100644 --- a/src/logos.rs +++ b/src/logos.rs @@ -6,15 +6,14 @@ pub const RADARR_LOGO: &str = "⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⢿⡇⠀⠀⣀⣤⣶⡿⠛⠉⠀⠀⠀⠀ ⠀⠀⠰⠶⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀ "; -// Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then -#[allow(dead_code)] -pub const SONARR_LOGO: &str = "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⢀⣄⠙⠻⠟⠋⣤⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⢸⣿⠆⢾⡗⢸⣿⡇⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⠈⠋⣠⣴⣦⣄⠛⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +pub const SONARR_LOGO: &str = "⠀⠀⠀⣠⣴⣶⣿⣿⣿⣿⣶⣦⣄⠀⠀⠀ +⠀⣠⡀⠈⠻⣿⣿⣿⣿⣿⣿⠟⠁⢀⣀⠀ +⢰⣿⣿⣦⠐⠄⠉⠉⠉⠉⠠⠂⣰⣿⣿⡆ +⣿⣿⣿⣿⡆⠀⣴⣿⣿⣦⠀⢰⣿⣿⣿⣿ +⣿⣿⣿⣿⡇⠀⠻⣿⣿⠟⠀⠸⣿⣿⣿⣿ +⠸⣿⣿⠟⠠⠂⠀⢀⡀⠀⠐⠄⠻⣿⣿⠇ +⠀⠙⠁⢀⣴⣾⣿⣿⣿⣿⣷⣦⡀⠈⠋⠀ +⠀⠀⠀⠘⠻⠿⣿⣿⣿⣿⠿⠟⠋⠀⠀⠀ "; // Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then #[allow(dead_code)] diff --git a/src/main.rs b/src/main.rs index d828b28..a047046 100644 --- a/src/main.rs +++ b/src/main.rs @@ -177,7 +177,6 @@ async fn start_ui(app: &Arc>>) -> Result<()> { terminal.hide_cursor()?; let input_events = Events::new(); - let mut is_first_render = true; loop { let mut app = app.lock().await; @@ -193,10 +192,8 @@ async fn start_ui(app: &Arc>>) -> Result<()> { handlers::handle_events(key, &mut app); } - InputEvent::Tick => app.on_tick(is_first_render).await, + InputEvent::Tick => app.on_tick().await, } - - is_first_render = false; } terminal.show_cursor()?; diff --git a/src/models/mod.rs b/src/models/mod.rs index d0aa2c9..ba2a3f9 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -290,8 +290,8 @@ impl TabState { &self.tabs[self.index] } - pub fn get_active_route(&self) -> &Route { - &self.tabs[self.index].route + pub fn get_active_route(&self) -> Route { + self.tabs[self.index].route } pub fn get_active_tab_help(&self) -> &str { @@ -320,33 +320,46 @@ pub struct BlockSelectionState<'a, T> where T: Sized + Clone + Copy + Default, { - pub blocks: &'a [T], - pub index: usize, + pub blocks: &'a [&'a [T]], + pub x: usize, + pub y: usize, } impl<'a, T> BlockSelectionState<'a, T> where T: Sized + Clone + Copy + Default, { - pub fn new(blocks: &'a [T]) -> BlockSelectionState<'a, T> { - BlockSelectionState { blocks, index: 0 } + pub fn new(blocks: &'a [&'a [T]]) -> BlockSelectionState<'a, T> { + BlockSelectionState { blocks, x: 0, y: 0 } } - pub fn get_active_block(&self) -> &T { - &self.blocks[self.index] + pub fn get_active_block(&self) -> T { + self.blocks[self.y][self.x] } - pub fn next(&mut self) { - self.index = (self.index + 1) % self.blocks.len(); - } - - pub fn previous(&mut self) { - if self.index > 0 { - self.index -= 1; + pub fn left(&mut self) { + if self.x > 0 { + self.x -= 1; } else { - self.index = self.blocks.len() - 1; + self.x = self.blocks[0].len() - 1; } } + + pub fn right(&mut self) { + self.x = (self.x + 1) % self.blocks[0].len(); + } + + pub fn up(&mut self) { + if self.y > 0 { + self.y -= 1; + } else { + self.y = self.blocks.len() - 1; + } + } + + pub fn down(&mut self) { + self.y = (self.y + 1) % self.blocks.len(); + } } #[cfg(test)] @@ -354,8 +367,9 @@ impl<'a, T> BlockSelectionState<'a, T> where T: Sized + Clone + Copy + Default, { - pub fn set_index(&mut self, index: usize) { - self.index = index; + pub fn set_index(&mut self, x: usize, y: usize) { + self.x = x; + self.y = y; } } diff --git a/src/models/model_tests.rs b/src/models/model_tests.rs index 58880fc..0514577 100644 --- a/src/models/model_tests.rs +++ b/src/models/model_tests.rs @@ -17,14 +17,7 @@ mod tests { BlockSelectionState, HorizontallyScrollableText, Scrollable, ScrollableText, TabRoute, TabState, }; - const BLOCKS: [ActiveRadarrBlock; 6] = [ - ActiveRadarrBlock::AddMovieSelectRootFolder, - ActiveRadarrBlock::AddMovieSelectMonitor, - ActiveRadarrBlock::AddMovieSelectMinimumAvailability, - ActiveRadarrBlock::AddMovieSelectQualityProfile, - ActiveRadarrBlock::AddMovieTagsInput, - ActiveRadarrBlock::AddMovieConfirmPrompt, - ]; + const BLOCKS: &[&[i32]] = &[&[11, 12], &[21, 22], &[31, 32]]; #[test] fn test_scrollable_text_with_string() { @@ -517,7 +510,7 @@ mod tests { let active_route = tab_state.get_active_route(); - assert_eq!(active_route, &second_tab); + assert_eq!(active_route, second_tab); } #[test] @@ -548,15 +541,15 @@ mod tests { let tab_routes = create_test_tab_routes(); let mut tab_state = TabState::new(create_test_tab_routes()); - assert_eq!(tab_state.get_active_route(), &tab_routes[0].route); + assert_eq!(tab_state.get_active_route(), tab_routes[0].route); tab_state.next(); - assert_eq!(tab_state.get_active_route(), &tab_routes[1].route); + assert_eq!(tab_state.get_active_route(), tab_routes[1].route); tab_state.next(); - assert_eq!(tab_state.get_active_route(), &tab_routes[0].route); + assert_eq!(tab_state.get_active_route(), tab_routes[0].route); } #[test] @@ -564,73 +557,129 @@ mod tests { let tab_routes = create_test_tab_routes(); let mut tab_state = TabState::new(create_test_tab_routes()); - assert_eq!(tab_state.get_active_route(), &tab_routes[0].route); + assert_eq!(tab_state.get_active_route(), tab_routes[0].route); tab_state.previous(); - assert_eq!(tab_state.get_active_route(), &tab_routes[1].route); + assert_eq!(tab_state.get_active_route(), tab_routes[1].route); tab_state.previous(); - assert_eq!(tab_state.get_active_route(), &tab_routes[0].route); + assert_eq!(tab_state.get_active_route(), tab_routes[0].route); } #[test] fn test_block_selection_state_new() { - let block_selection_state = BlockSelectionState::new(&BLOCKS); + let block_selection_state = BlockSelectionState::new(BLOCKS); - assert_eq!(block_selection_state.index, 0); + assert_eq!(block_selection_state.x, 0); + assert_eq!(block_selection_state.y, 0); } #[test] fn test_block_selection_state_get_active_block() { - let second_block = BLOCKS[1]; + let second_block = BLOCKS[1][1]; let block_selection_state = BlockSelectionState { - blocks: &BLOCKS, - index: 1, + blocks: BLOCKS, + x: 1, + y: 1, }; let active_block = block_selection_state.get_active_block(); - assert_eq!(active_block, &second_block); + assert_eq!(active_block, second_block); } #[test] - fn test_block_selection_state_next() { - let blocks = [ - ActiveRadarrBlock::AddMovieSelectRootFolder, - ActiveRadarrBlock::AddMovieSelectMonitor, - ]; - let mut block_selection_state = BlockSelectionState::new(&blocks); + fn test_block_selection_state_down() { + let mut block_selection_state = BlockSelectionState::new(BLOCKS); - assert_eq!(block_selection_state.get_active_block(), &blocks[0]); + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][0]); - block_selection_state.next(); + block_selection_state.down(); - assert_eq!(block_selection_state.get_active_block(), &blocks[1]); + assert_eq!(block_selection_state.get_active_block(), BLOCKS[1][0]); - block_selection_state.next(); + block_selection_state.down(); - assert_eq!(block_selection_state.get_active_block(), &blocks[0]); + assert_eq!(block_selection_state.get_active_block(), BLOCKS[2][0]); + + block_selection_state.down(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][0]); } #[test] - fn test_block_selection_state_previous() { - let blocks = [ - ActiveRadarrBlock::AddMovieSelectRootFolder, - ActiveRadarrBlock::AddMovieSelectMonitor, - ]; - let mut block_selection_state = BlockSelectionState::new(&blocks); + fn test_block_selection_state_up() { + let mut block_selection_state = BlockSelectionState::new(BLOCKS); - assert_eq!(block_selection_state.get_active_block(), &blocks[0]); + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][0]); - block_selection_state.previous(); + block_selection_state.up(); - assert_eq!(block_selection_state.get_active_block(), &blocks[1]); + assert_eq!(block_selection_state.get_active_block(), BLOCKS[2][0]); - block_selection_state.previous(); + block_selection_state.up(); - assert_eq!(block_selection_state.get_active_block(), &blocks[0]); + assert_eq!(block_selection_state.get_active_block(), BLOCKS[1][0]); + + block_selection_state.up(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][0]); + } + + #[test] + fn test_block_selection_state_left() { + let mut block_selection_state = BlockSelectionState::new(BLOCKS); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][0]); + + block_selection_state.left(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][1]); + + block_selection_state.left(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][0]); + + block_selection_state.set_index(0, 1); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[1][0]); + + block_selection_state.left(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[1][1]); + + block_selection_state.left(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[1][0]); + } + + #[test] + fn test_block_selection_state_right() { + let mut block_selection_state = BlockSelectionState::new(BLOCKS); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][0]); + + block_selection_state.right(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][1]); + + block_selection_state.right(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[0][0]); + + block_selection_state.set_index(0, 1); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[1][0]); + + block_selection_state.right(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[1][1]); + + block_selection_state.right(); + + assert_eq!(block_selection_state.get_active_block(), BLOCKS[1][0]); } #[test] diff --git a/src/models/servarr_data/modals.rs b/src/models/servarr_data/modals.rs index 0105249..46d329b 100644 --- a/src/models/servarr_data/modals.rs +++ b/src/models/servarr_data/modals.rs @@ -10,6 +10,7 @@ pub struct EditIndexerModal { pub api_key: HorizontallyScrollableText, pub seed_ratio: HorizontallyScrollableText, pub tags: HorizontallyScrollableText, + pub priority: i64, } #[derive(Default, Clone, Eq, PartialEq, Debug)] diff --git a/src/models/servarr_data/radarr/modals.rs b/src/models/servarr_data/radarr/modals.rs index 1b9c0d9..b4f7f73 100644 --- a/src/models/servarr_data/radarr/modals.rs +++ b/src/models/servarr_data/radarr/modals.rs @@ -36,6 +36,7 @@ impl From<&RadarrData<'_>> for EditIndexerModal { enable_interactive_search, tags, fields, + priority, .. } = radarr_data.indexers.current_selection(); let seed_ratio_field_option = fields @@ -53,6 +54,7 @@ impl From<&RadarrData<'_>> for EditIndexerModal { edit_indexer_modal.enable_rss = Some(*enable_rss); edit_indexer_modal.enable_automatic_search = Some(*enable_automatic_search); edit_indexer_modal.enable_interactive_search = Some(*enable_interactive_search); + edit_indexer_modal.priority = *priority; edit_indexer_modal.url = fields .as_ref() .unwrap() diff --git a/src/models/servarr_data/radarr/modals_tests.rs b/src/models/servarr_data/radarr/modals_tests.rs index e19d8a9..91b920b 100644 --- a/src/models/servarr_data/radarr/modals_tests.rs +++ b/src/models/servarr_data/radarr/modals_tests.rs @@ -45,6 +45,7 @@ mod test { enable_interactive_search: true, tags: vec![Number::from(1), Number::from(2)], fields: Some(fields), + priority: 1, ..Indexer::default() }; radarr_data.indexers.set_items(vec![indexer]); @@ -55,6 +56,7 @@ mod test { assert_eq!(edit_indexer_modal.enable_rss, Some(true)); assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true)); assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true)); + assert_eq!(edit_indexer_modal.priority, 1); assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); @@ -93,6 +95,7 @@ mod test { enable_interactive_search: true, tags: vec![Number::from(1), Number::from(2)], fields: Some(fields), + priority: 1, ..Indexer::default() }; radarr_data.indexers.set_items(vec![indexer]); @@ -103,6 +106,7 @@ mod test { assert_eq!(edit_indexer_modal.enable_rss, Some(true)); assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true)); assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true)); + assert_eq!(edit_indexer_modal.priority, 1); assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); assert!(edit_indexer_modal.seed_ratio.text.is_empty()); diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index dcae501..d5ae110 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -1,9 +1,10 @@ -use crate::app::context_clues::build_context_clue_string; +use crate::app::context_clues::{ + build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, +}; use crate::app::radarr::radarr_context_clues::{ - BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, - INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, - MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, - SYSTEM_CONTEXT_CLUES, + COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, + MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, }; use crate::models::radarr_models::{ AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord, @@ -61,7 +62,7 @@ pub struct RadarrData<'a> { pub edit_indexer_modal: Option, pub edit_root_folder: Option, pub indexer_settings: Option, - pub indexer_test_error: Option, + pub indexer_test_errors: Option, pub indexer_test_all_results: Option>, pub movie_details_modal: Option, pub prompt_confirm: bool, @@ -111,7 +112,7 @@ impl<'a> Default for RadarrData<'a> { edit_indexer_modal: None, edit_root_folder: None, indexer_settings: None, - indexer_test_error: None, + indexer_test_errors: None, indexer_test_all_results: None, movie_details_modal: None, prompt_confirm: false, @@ -208,7 +209,6 @@ impl<'a> Default for RadarrData<'a> { #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)] pub enum ActiveRadarrBlock { - AddIndexer, AddMovieAlreadyInLibrary, AddMovieSearchInput, AddMovieSearchResults, @@ -255,6 +255,7 @@ pub enum ActiveRadarrBlock { EditIndexerToggleEnableRss, EditIndexerToggleEnableAutomaticSearch, EditIndexerToggleEnableInteractiveSearch, + EditIndexerPriorityInput, EditIndexerUrlInput, EditIndexerTagsInput, EditMoviePrompt, @@ -326,8 +327,7 @@ pub static COLLECTIONS_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::FilterCollectionsError, ActiveRadarrBlock::UpdateAllCollectionsPrompt, ]; -pub static INDEXERS_BLOCKS: [ActiveRadarrBlock; 4] = [ - ActiveRadarrBlock::AddIndexer, +pub static INDEXERS_BLOCKS: [ActiveRadarrBlock; 3] = [ ActiveRadarrBlock::DeleteIndexerPrompt, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::TestIndexer, @@ -356,13 +356,13 @@ pub static ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::AddMovieAlreadyInLibrary, ActiveRadarrBlock::AddMovieTagsInput, ]; -pub static ADD_MOVIE_SELECTION_BLOCKS: [ActiveRadarrBlock; 6] = [ - ActiveRadarrBlock::AddMovieSelectRootFolder, - ActiveRadarrBlock::AddMovieSelectMonitor, - ActiveRadarrBlock::AddMovieSelectMinimumAvailability, - ActiveRadarrBlock::AddMovieSelectQualityProfile, - ActiveRadarrBlock::AddMovieTagsInput, - ActiveRadarrBlock::AddMovieConfirmPrompt, +pub const ADD_MOVIE_SELECTION_BLOCKS: &[&[ActiveRadarrBlock]] = &[ + &[ActiveRadarrBlock::AddMovieSelectRootFolder], + &[ActiveRadarrBlock::AddMovieSelectMonitor], + &[ActiveRadarrBlock::AddMovieSelectMinimumAvailability], + &[ActiveRadarrBlock::AddMovieSelectQualityProfile], + &[ActiveRadarrBlock::AddMovieTagsInput], + &[ActiveRadarrBlock::AddMovieConfirmPrompt], ]; pub static EDIT_COLLECTION_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::EditCollectionPrompt, @@ -373,13 +373,13 @@ pub static EDIT_COLLECTION_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::EditCollectionToggleSearchOnAdd, ActiveRadarrBlock::EditCollectionToggleMonitored, ]; -pub static EDIT_COLLECTION_SELECTION_BLOCKS: [ActiveRadarrBlock; 6] = [ - ActiveRadarrBlock::EditCollectionToggleMonitored, - ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, - ActiveRadarrBlock::EditCollectionSelectQualityProfile, - ActiveRadarrBlock::EditCollectionRootFolderPathInput, - ActiveRadarrBlock::EditCollectionToggleSearchOnAdd, - ActiveRadarrBlock::EditCollectionConfirmPrompt, +pub const EDIT_COLLECTION_SELECTION_BLOCKS: &[&[ActiveRadarrBlock]] = &[ + &[ActiveRadarrBlock::EditCollectionToggleMonitored], + &[ActiveRadarrBlock::EditCollectionSelectMinimumAvailability], + &[ActiveRadarrBlock::EditCollectionSelectQualityProfile], + &[ActiveRadarrBlock::EditCollectionRootFolderPathInput], + &[ActiveRadarrBlock::EditCollectionToggleSearchOnAdd], + &[ActiveRadarrBlock::EditCollectionConfirmPrompt], ]; pub static EDIT_MOVIE_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::EditMoviePrompt, @@ -390,13 +390,13 @@ pub static EDIT_MOVIE_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::EditMovieTagsInput, ActiveRadarrBlock::EditMovieToggleMonitored, ]; -pub static EDIT_MOVIE_SELECTION_BLOCKS: [ActiveRadarrBlock; 6] = [ - ActiveRadarrBlock::EditMovieToggleMonitored, - ActiveRadarrBlock::EditMovieSelectMinimumAvailability, - ActiveRadarrBlock::EditMovieSelectQualityProfile, - ActiveRadarrBlock::EditMoviePathInput, - ActiveRadarrBlock::EditMovieTagsInput, - ActiveRadarrBlock::EditMovieConfirmPrompt, +pub const EDIT_MOVIE_SELECTION_BLOCKS: &[&[ActiveRadarrBlock]] = &[ + &[ActiveRadarrBlock::EditMovieToggleMonitored], + &[ActiveRadarrBlock::EditMovieSelectMinimumAvailability], + &[ActiveRadarrBlock::EditMovieSelectQualityProfile], + &[ActiveRadarrBlock::EditMoviePathInput], + &[ActiveRadarrBlock::EditMovieTagsInput], + &[ActiveRadarrBlock::EditMovieConfirmPrompt], ]; pub static DOWNLOADS_BLOCKS: [ActiveRadarrBlock; 3] = [ ActiveRadarrBlock::Downloads, @@ -425,12 +425,12 @@ pub static DELETE_MOVIE_BLOCKS: [ActiveRadarrBlock; 4] = [ ActiveRadarrBlock::DeleteMovieToggleDeleteFile, ActiveRadarrBlock::DeleteMovieToggleAddListExclusion, ]; -pub static DELETE_MOVIE_SELECTION_BLOCKS: [ActiveRadarrBlock; 3] = [ - ActiveRadarrBlock::DeleteMovieToggleDeleteFile, - ActiveRadarrBlock::DeleteMovieToggleAddListExclusion, - ActiveRadarrBlock::DeleteMovieConfirmPrompt, +pub const DELETE_MOVIE_SELECTION_BLOCKS: &[&[ActiveRadarrBlock]] = &[ + &[ActiveRadarrBlock::DeleteMovieToggleDeleteFile], + &[ActiveRadarrBlock::DeleteMovieToggleAddListExclusion], + &[ActiveRadarrBlock::DeleteMovieConfirmPrompt], ]; -pub static EDIT_INDEXER_BLOCKS: [ActiveRadarrBlock; 10] = [ +pub static EDIT_INDEXER_BLOCKS: [ActiveRadarrBlock; 11] = [ ActiveRadarrBlock::EditIndexerPrompt, ActiveRadarrBlock::EditIndexerConfirmPrompt, ActiveRadarrBlock::EditIndexerApiKeyInput, @@ -439,32 +439,57 @@ pub static EDIT_INDEXER_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::EditIndexerToggleEnableRss, ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerPriorityInput, ActiveRadarrBlock::EditIndexerUrlInput, ActiveRadarrBlock::EditIndexerTagsInput, ]; -pub static EDIT_INDEXER_TORRENT_SELECTION_BLOCKS: [ActiveRadarrBlock; 10] = [ - ActiveRadarrBlock::EditIndexerNameInput, - ActiveRadarrBlock::EditIndexerToggleEnableRss, - ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, - ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, - ActiveRadarrBlock::EditIndexerConfirmPrompt, - ActiveRadarrBlock::EditIndexerUrlInput, - ActiveRadarrBlock::EditIndexerApiKeyInput, - ActiveRadarrBlock::EditIndexerSeedRatioInput, - ActiveRadarrBlock::EditIndexerTagsInput, - ActiveRadarrBlock::EditIndexerConfirmPrompt, +pub const EDIT_INDEXER_TORRENT_SELECTION_BLOCKS: &[&[ActiveRadarrBlock]] = &[ + &[ + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + ], + &[ + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveRadarrBlock::EditIndexerPriorityInput, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ], + &[ + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ], ]; -pub static EDIT_INDEXER_NZB_SELECTION_BLOCKS: [ActiveRadarrBlock; 10] = [ - ActiveRadarrBlock::EditIndexerNameInput, - ActiveRadarrBlock::EditIndexerToggleEnableRss, - ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, - ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, - ActiveRadarrBlock::EditIndexerConfirmPrompt, - ActiveRadarrBlock::EditIndexerUrlInput, - ActiveRadarrBlock::EditIndexerApiKeyInput, - ActiveRadarrBlock::EditIndexerTagsInput, - ActiveRadarrBlock::EditIndexerConfirmPrompt, - ActiveRadarrBlock::EditIndexerConfirmPrompt, +pub const EDIT_INDEXER_NZB_SELECTION_BLOCKS: &[&[ActiveRadarrBlock]] = &[ + &[ + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerPriorityInput, + ], + &[ + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ], ]; pub static INDEXER_SETTINGS_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::AllIndexerSettingsPrompt, @@ -478,17 +503,27 @@ pub static INDEXER_SETTINGS_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags, ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, ]; -pub static INDEXER_SETTINGS_SELECTION_BLOCKS: [ActiveRadarrBlock; 10] = [ - ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, - ActiveRadarrBlock::IndexerSettingsRetentionInput, - ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, - ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags, - ActiveRadarrBlock::IndexerSettingsConfirmPrompt, - ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, - ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, - ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs, - ActiveRadarrBlock::IndexerSettingsConfirmPrompt, +pub const INDEXER_SETTINGS_SELECTION_BLOCKS: &[&[ActiveRadarrBlock]] = &[ + &[ + ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, + ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, + ], + &[ + ActiveRadarrBlock::IndexerSettingsRetentionInput, + ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, + ], + &[ + ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + ], + &[ + ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags, + ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs, + ], + &[ + ActiveRadarrBlock::IndexerSettingsConfirmPrompt, + ActiveRadarrBlock::IndexerSettingsConfirmPrompt, + ], ]; pub static SYSTEM_DETAILS_BLOCKS: [ActiveRadarrBlock; 5] = [ ActiveRadarrBlock::SystemLogs, diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index 1c42d9f..04590c9 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -4,12 +4,14 @@ mod tests { use chrono::{DateTime, Utc}; use pretty_assertions::{assert_eq, assert_str_eq}; - use crate::app::context_clues::build_context_clue_string; + use crate::app::context_clues::{ + build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + }; use crate::app::radarr::radarr_context_clues::{ - BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, - INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, - MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, - SYSTEM_CONTEXT_CLUES, + COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, + MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, + MOVIE_DETAILS_CONTEXT_CLUES, }; use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils; @@ -90,7 +92,7 @@ mod tests { assert!(radarr_data.edit_root_folder.is_none()); assert!(radarr_data.edit_indexer_modal.is_none()); assert!(radarr_data.indexer_settings.is_none()); - assert!(radarr_data.indexer_test_error.is_none()); + assert!(radarr_data.indexer_test_errors.is_none()); assert!(radarr_data.indexer_test_all_results.is_none()); assert!(radarr_data.movie_details_modal.is_none()); assert!(radarr_data.prompt_confirm_action.is_none()); @@ -301,8 +303,7 @@ mod tests { #[test] fn test_indexers_blocks_contents() { - assert_eq!(INDEXERS_BLOCKS.len(), 4); - assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::AddIndexer)); + assert_eq!(INDEXERS_BLOCKS.len(), 3); assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::DeleteIndexerPrompt)); assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::Indexers)); assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::TestIndexer)); @@ -411,7 +412,7 @@ mod tests { #[test] fn test_edit_indexer_blocks_contents() { - assert_eq!(EDIT_INDEXER_BLOCKS.len(), 10); + assert_eq!(EDIT_INDEXER_BLOCKS.len(), 11); assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerPrompt)); assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerConfirmPrompt)); assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerApiKeyInput)); @@ -426,6 +427,7 @@ mod tests { ); assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerUrlInput)); assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerTagsInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerPriorityInput)); } #[test] @@ -466,27 +468,27 @@ mod tests { assert_eq!( add_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::AddMovieSelectRootFolder + &[ActiveRadarrBlock::AddMovieSelectRootFolder] ); assert_eq!( add_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::AddMovieSelectMonitor + &[ActiveRadarrBlock::AddMovieSelectMonitor] ); assert_eq!( add_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::AddMovieSelectMinimumAvailability + &[ActiveRadarrBlock::AddMovieSelectMinimumAvailability] ); assert_eq!( add_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::AddMovieSelectQualityProfile + &[ActiveRadarrBlock::AddMovieSelectQualityProfile] ); assert_eq!( add_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::AddMovieTagsInput + &[ActiveRadarrBlock::AddMovieTagsInput] ); assert_eq!( add_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::AddMovieConfirmPrompt + &[ActiveRadarrBlock::AddMovieConfirmPrompt] ); assert_eq!(add_movie_block_iter.next(), None); } @@ -497,27 +499,27 @@ mod tests { assert_eq!( edit_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditMovieToggleMonitored + &[ActiveRadarrBlock::EditMovieToggleMonitored] ); assert_eq!( edit_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditMovieSelectMinimumAvailability + &[ActiveRadarrBlock::EditMovieSelectMinimumAvailability] ); assert_eq!( edit_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditMovieSelectQualityProfile + &[ActiveRadarrBlock::EditMovieSelectQualityProfile] ); assert_eq!( edit_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditMoviePathInput + &[ActiveRadarrBlock::EditMoviePathInput] ); assert_eq!( edit_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditMovieTagsInput + &[ActiveRadarrBlock::EditMovieTagsInput] ); assert_eq!( edit_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditMovieConfirmPrompt + &[ActiveRadarrBlock::EditMovieConfirmPrompt] ); assert_eq!(edit_movie_block_iter.next(), None); } @@ -528,27 +530,27 @@ mod tests { assert_eq!( edit_collection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditCollectionToggleMonitored + &[ActiveRadarrBlock::EditCollectionToggleMonitored] ); assert_eq!( edit_collection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditCollectionSelectMinimumAvailability + &[ActiveRadarrBlock::EditCollectionSelectMinimumAvailability] ); assert_eq!( edit_collection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditCollectionSelectQualityProfile + &[ActiveRadarrBlock::EditCollectionSelectQualityProfile] ); assert_eq!( edit_collection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditCollectionRootFolderPathInput + &[ActiveRadarrBlock::EditCollectionRootFolderPathInput] ); assert_eq!( edit_collection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditCollectionToggleSearchOnAdd + &[ActiveRadarrBlock::EditCollectionToggleSearchOnAdd] ); assert_eq!( edit_collection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditCollectionConfirmPrompt + &[ActiveRadarrBlock::EditCollectionConfirmPrompt] ); assert_eq!(edit_collection_block_iter.next(), None); } @@ -559,15 +561,15 @@ mod tests { assert_eq!( delete_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::DeleteMovieToggleDeleteFile + &[ActiveRadarrBlock::DeleteMovieToggleDeleteFile] ); assert_eq!( delete_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::DeleteMovieToggleAddListExclusion + &[ActiveRadarrBlock::DeleteMovieToggleAddListExclusion] ); assert_eq!( delete_movie_block_iter.next().unwrap(), - &ActiveRadarrBlock::DeleteMovieConfirmPrompt + &[ActiveRadarrBlock::DeleteMovieConfirmPrompt] ); assert_eq!(delete_movie_block_iter.next(), None); } @@ -579,43 +581,45 @@ mod tests { assert_eq!( edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerNameInput + &[ + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerUrlInput, + ] ); assert_eq!( edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerToggleEnableRss + &[ + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ] ); assert_eq!( edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch + &[ + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + ] ); assert_eq!( edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch + &[ + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerTagsInput, + ] ); assert_eq!( edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerConfirmPrompt + &[ + ActiveRadarrBlock::EditIndexerPriorityInput, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ] ); assert_eq!( edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerUrlInput - ); - assert_eq!( - edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerApiKeyInput - ); - assert_eq!( - edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerSeedRatioInput - ); - assert_eq!( - edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerTagsInput - ); - assert_eq!( - edit_indexer_torrent_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerConfirmPrompt + &[ + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ] ); assert_eq!(edit_indexer_torrent_selection_block_iter.next(), None); } @@ -626,43 +630,38 @@ mod tests { assert_eq!( edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerNameInput + &[ + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerUrlInput, + ] ); assert_eq!( edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerToggleEnableRss + &[ + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ] ); assert_eq!( edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch + &[ + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerTagsInput, + ] ); assert_eq!( edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch + &[ + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerPriorityInput, + ] ); assert_eq!( edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerConfirmPrompt - ); - assert_eq!( - edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerUrlInput - ); - assert_eq!( - edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerApiKeyInput - ); - assert_eq!( - edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerTagsInput - ); - assert_eq!( - edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerConfirmPrompt - ); - assert_eq!( - edit_indexer_nzb_selection_block_iter.next().unwrap(), - &ActiveRadarrBlock::EditIndexerConfirmPrompt + &[ + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ] ); assert_eq!(edit_indexer_nzb_selection_block_iter.next(), None); } @@ -673,43 +672,38 @@ mod tests { assert_eq!( indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput + &[ + ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, + ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, + ] ); assert_eq!( indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsRetentionInput + &[ + ActiveRadarrBlock::IndexerSettingsRetentionInput, + ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, + ] ); assert_eq!( indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput + &[ + ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + ] ); assert_eq!( indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags + &[ + ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags, + ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs, + ] ); assert_eq!( indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsConfirmPrompt - ); - assert_eq!( - indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput - ); - assert_eq!( - indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput - ); - assert_eq!( - indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput - ); - assert_eq!( - indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs - ); - assert_eq!( - indexer_settings_block_iter.next().unwrap(), - &ActiveRadarrBlock::IndexerSettingsConfirmPrompt + &[ + ActiveRadarrBlock::IndexerSettingsConfirmPrompt, + ActiveRadarrBlock::IndexerSettingsConfirmPrompt, + ] ); assert_eq!(indexer_settings_block_iter.next(), None); } diff --git a/src/models/servarr_data/radarr/radarr_test_utils.rs b/src/models/servarr_data/radarr/radarr_test_utils.rs index a3c5469..1f00a3d 100644 --- a/src/models/servarr_data/radarr/radarr_test_utils.rs +++ b/src/models/servarr_data/radarr/radarr_test_utils.rs @@ -1,10 +1,12 @@ #[cfg(test)] pub mod utils { use crate::models::radarr_models::{ - AddMovieSearchResult, CollectionMovie, Credit, MovieHistoryItem, RadarrRelease, + AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, Credit, DownloadRecord, + Movie, MovieHistoryItem, RadarrRelease, }; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::radarr_data::RadarrData; + use crate::models::servarr_models::{Indexer, RootFolder}; use crate::models::stateful_table::StatefulTable; use crate::models::{HorizontallyScrollableText, ScrollableText}; @@ -35,6 +37,23 @@ pub mod utils { add_searched_movies: Some(StatefulTable::default()), ..RadarrData::default() }; + radarr_data.movies.set_items(vec![Movie::default()]); + radarr_data + .collection_movies + .set_items(vec![CollectionMovie::default()]); + radarr_data + .collections + .set_items(vec![Collection::default()]); + radarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + radarr_data + .blocklist + .set_items(vec![BlocklistItem::default()]); + radarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + radarr_data.indexers.set_items(vec![Indexer::default()]); radarr_data.movie_info_tabs.index = 1; radarr_data .add_searched_movies diff --git a/src/models/servarr_data/sonarr/modals.rs b/src/models/servarr_data/sonarr/modals.rs index c41b58b..4936789 100644 --- a/src/models/servarr_data/sonarr/modals.rs +++ b/src/models/servarr_data/sonarr/modals.rs @@ -1,16 +1,27 @@ use strum::IntoEnumIterator; -use crate::models::{ - servarr_data::modals::EditIndexerModal, - servarr_models::{Indexer, RootFolder}, - sonarr_models::{Episode, Series, SeriesMonitor, SeriesType, SonarrHistoryItem, SonarrRelease}, - stateful_list::StatefulList, - stateful_table::StatefulTable, - HorizontallyScrollableText, ScrollableText, +use super::sonarr_data::{ActiveSonarrBlock, SonarrData}; +use crate::models::sonarr_models::EpisodeFile; +use crate::{ + app::{ + context_clues::build_context_clue_string, + sonarr::sonarr_context_clues::{ + DETAILS_CONTEXTUAL_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, + MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, + SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, + SEASON_HISTORY_CONTEXT_CLUES, + }, + }, + models::{ + servarr_data::modals::EditIndexerModal, + servarr_models::{Indexer, RootFolder}, + sonarr_models::{Episode, Series, SeriesMonitor, SeriesType, SonarrHistoryItem, SonarrRelease}, + stateful_list::StatefulList, + stateful_table::StatefulTable, + HorizontallyScrollableText, ScrollableText, TabRoute, TabState, + }, }; -use super::sonarr_data::SonarrData; - #[cfg(test)] #[path = "modals_tests.rs"] mod modals_tests; @@ -26,8 +37,8 @@ pub struct AddSeriesModal { pub tags: HorizontallyScrollableText, } -impl From<&SonarrData> for AddSeriesModal { - fn from(sonarr_data: &SonarrData) -> AddSeriesModal { +impl From<&SonarrData<'_>> for AddSeriesModal { + fn from(sonarr_data: &SonarrData<'_>) -> AddSeriesModal { let mut add_series_modal = AddSeriesModal { use_season_folder: true, ..AddSeriesModal::default() @@ -64,8 +75,8 @@ impl From<&SonarrData> for AddSeriesModal { } } -impl From<&SonarrData> for EditIndexerModal { - fn from(sonarr_data: &SonarrData) -> EditIndexerModal { +impl From<&SonarrData<'_>> for EditIndexerModal { + fn from(sonarr_data: &SonarrData<'_>) -> EditIndexerModal { let mut edit_indexer_modal = EditIndexerModal::default(); let Indexer { name, @@ -74,6 +85,7 @@ impl From<&SonarrData> for EditIndexerModal { enable_interactive_search, tags, fields, + priority, .. } = sonarr_data.indexers.current_selection(); let seed_ratio_field_option = fields @@ -91,6 +103,7 @@ impl From<&SonarrData> for EditIndexerModal { edit_indexer_modal.enable_rss = Some(*enable_rss); edit_indexer_modal.enable_automatic_search = Some(*enable_automatic_search); edit_indexer_modal.enable_interactive_search = Some(*enable_interactive_search); + edit_indexer_modal.priority = *priority; edit_indexer_modal.url = fields .as_ref() .unwrap() @@ -153,8 +166,8 @@ pub struct EditSeriesModal { pub tags: HorizontallyScrollableText, } -impl From<&SonarrData> for EditSeriesModal { - fn from(sonarr_data: &SonarrData) -> EditSeriesModal { +impl From<&SonarrData<'_>> for EditSeriesModal { + fn from(sonarr_data: &SonarrData<'_>) -> EditSeriesModal { let mut edit_series_modal = EditSeriesModal::default(); let Series { path, @@ -246,22 +259,94 @@ impl From<&SonarrData> for EditSeriesModal { } } -#[derive(Default)] pub struct EpisodeDetailsModal { - // Temporarily allowing this, since the value is only current written and not read. - // This will be read from once I begin the UI work for Sonarr - #[allow(dead_code)] pub episode_details: ScrollableText, pub file_details: String, pub audio_details: String, pub video_details: String, pub episode_history: StatefulTable, pub episode_releases: StatefulTable, + pub episode_details_tabs: TabState, +} + +impl Default for EpisodeDetailsModal { + fn default() -> EpisodeDetailsModal { + EpisodeDetailsModal { + episode_details: ScrollableText::default(), + file_details: String::new(), + audio_details: String::new(), + video_details: String::new(), + episode_history: StatefulTable::default(), + episode_releases: StatefulTable::default(), + episode_details_tabs: TabState::new(vec![ + TabRoute { + title: "Details", + route: ActiveSonarrBlock::EpisodeDetails.into(), + help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), + contextual_help: None, + }, + TabRoute { + title: "History", + route: ActiveSonarrBlock::EpisodeHistory.into(), + help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), + contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + }, + TabRoute { + title: "File", + route: ActiveSonarrBlock::EpisodeFile.into(), + help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), + contextual_help: None, + }, + TabRoute { + title: "Manual Search", + route: ActiveSonarrBlock::ManualEpisodeSearch.into(), + help: build_context_clue_string(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES), + contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + }, + ]), + } + } } -#[derive(Default)] pub struct SeasonDetailsModal { pub episodes: StatefulTable, + pub episode_files: StatefulTable, pub episode_details_modal: Option, + pub season_history: StatefulTable, pub season_releases: StatefulTable, + pub season_details_tabs: TabState, +} + +impl Default for SeasonDetailsModal { + fn default() -> SeasonDetailsModal { + SeasonDetailsModal { + episodes: StatefulTable::default(), + episode_details_modal: None, + episode_files: StatefulTable::default(), + season_releases: StatefulTable::default(), + season_history: StatefulTable::default(), + season_details_tabs: TabState::new(vec![ + TabRoute { + title: "Episodes", + route: ActiveSonarrBlock::SeasonDetails.into(), + help: build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES), + contextual_help: Some(build_context_clue_string( + &SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, + )), + }, + TabRoute { + title: "History", + route: ActiveSonarrBlock::SeasonHistory.into(), + help: build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES), + contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + }, + TabRoute { + title: "Manual Search", + route: ActiveSonarrBlock::ManualSeasonSearch.into(), + help: build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES), + contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + }, + ]), + } + } } diff --git a/src/models/servarr_data/sonarr/modals_tests.rs b/src/models/servarr_data/sonarr/modals_tests.rs index 96af39f..5fa55e3 100644 --- a/src/models/servarr_data/sonarr/modals_tests.rs +++ b/src/models/servarr_data/sonarr/modals_tests.rs @@ -5,7 +5,17 @@ mod tests { use rstest::rstest; use strum::IntoEnumIterator; - use crate::models::servarr_data::sonarr::modals::EditSeriesModal; + use crate::app::context_clues::build_context_clue_string; + use crate::app::sonarr::sonarr_context_clues::{ + DETAILS_CONTEXTUAL_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, + MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, + SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, + SEASON_HISTORY_CONTEXT_CLUES, + }; + use crate::models::servarr_data::sonarr::modals::{ + EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal, + }; + use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_models::{Indexer, IndexerField}; use crate::models::{ servarr_data::sonarr::{modals::AddSeriesModal, sonarr_data::SonarrData}, @@ -95,6 +105,7 @@ mod tests { enable_interactive_search: true, tags: vec![Number::from(1), Number::from(2)], fields: Some(fields), + priority: 1, ..Indexer::default() }; sonarr_data.indexers.set_items(vec![indexer]); @@ -105,6 +116,7 @@ mod tests { assert_eq!(edit_indexer_modal.enable_rss, Some(true)); assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true)); assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true)); + assert_eq!(edit_indexer_modal.priority, 1); assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); @@ -143,6 +155,7 @@ mod tests { enable_interactive_search: true, tags: vec![Number::from(1), Number::from(2)], fields: Some(fields), + priority: 1, ..Indexer::default() }; sonarr_data.indexers.set_items(vec![indexer]); @@ -153,6 +166,7 @@ mod tests { assert_eq!(edit_indexer_modal.enable_rss, Some(true)); assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true)); assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true)); + assert_eq!(edit_indexer_modal.priority, 1); assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); assert!(edit_indexer_modal.seed_ratio.text.is_empty()); @@ -221,4 +235,150 @@ mod tests { assert_eq!(edit_series_modal.monitored, Some(true)); assert_eq!(edit_series_modal.use_season_folders, Some(true)); } + + #[test] + fn test_episode_details_modal_default() { + let episode_details_modal = EpisodeDetailsModal::default(); + + assert!(episode_details_modal.episode_details.is_empty()); + assert!(episode_details_modal.file_details.is_empty()); + assert!(episode_details_modal.audio_details.is_empty()); + assert!(episode_details_modal.video_details.is_empty()); + assert!(episode_details_modal.episode_history.is_empty()); + assert!(episode_details_modal.episode_releases.is_empty()); + + assert_eq!(episode_details_modal.episode_details_tabs.tabs.len(), 4); + + assert_str_eq!( + episode_details_modal.episode_details_tabs.tabs[0].title, + "Details" + ); + assert_eq!( + episode_details_modal.episode_details_tabs.tabs[0].route, + ActiveSonarrBlock::EpisodeDetails.into() + ); + assert_str_eq!( + episode_details_modal.episode_details_tabs.tabs[0].help, + build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES) + ); + assert!(episode_details_modal.episode_details_tabs.tabs[0] + .contextual_help + .is_none()); + + assert_str_eq!( + episode_details_modal.episode_details_tabs.tabs[1].title, + "History" + ); + assert_eq!( + episode_details_modal.episode_details_tabs.tabs[1].route, + ActiveSonarrBlock::EpisodeHistory.into() + ); + assert_str_eq!( + episode_details_modal.episode_details_tabs.tabs[1].help, + build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES) + ); + assert_eq!( + episode_details_modal.episode_details_tabs.tabs[1].contextual_help, + Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) + ); + + assert_str_eq!( + episode_details_modal.episode_details_tabs.tabs[2].title, + "File" + ); + assert_eq!( + episode_details_modal.episode_details_tabs.tabs[2].route, + ActiveSonarrBlock::EpisodeFile.into() + ); + assert_str_eq!( + episode_details_modal.episode_details_tabs.tabs[2].help, + build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES) + ); + assert!(episode_details_modal.episode_details_tabs.tabs[2] + .contextual_help + .is_none()); + + assert_str_eq!( + episode_details_modal.episode_details_tabs.tabs[3].title, + "Manual Search" + ); + assert_eq!( + episode_details_modal.episode_details_tabs.tabs[3].route, + ActiveSonarrBlock::ManualEpisodeSearch.into() + ); + assert_str_eq!( + episode_details_modal.episode_details_tabs.tabs[3].help, + build_context_clue_string(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES) + ); + assert_eq!( + episode_details_modal.episode_details_tabs.tabs[3].contextual_help, + Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) + ); + } + + #[test] + fn test_season_details_modal_default() { + let season_details_modal = SeasonDetailsModal::default(); + + assert!(season_details_modal.episodes.is_empty()); + assert!(season_details_modal.episode_details_modal.is_none()); + assert!(season_details_modal.episode_files.is_empty()); + assert!(season_details_modal.season_releases.is_empty()); + assert!(season_details_modal.season_history.is_empty()); + + assert_eq!(season_details_modal.season_details_tabs.tabs.len(), 3); + + assert_str_eq!( + season_details_modal.season_details_tabs.tabs[0].title, + "Episodes" + ); + assert_eq!( + season_details_modal.season_details_tabs.tabs[0].route, + ActiveSonarrBlock::SeasonDetails.into() + ); + assert_str_eq!( + season_details_modal.season_details_tabs.tabs[0].help, + build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES) + ); + assert_eq!( + season_details_modal.season_details_tabs.tabs[0].contextual_help, + Some(build_context_clue_string( + &SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES + )) + ); + + assert_str_eq!( + season_details_modal.season_details_tabs.tabs[1].title, + "History" + ); + assert_eq!( + season_details_modal.season_details_tabs.tabs[1].route, + ActiveSonarrBlock::SeasonHistory.into() + ); + assert_str_eq!( + season_details_modal.season_details_tabs.tabs[1].help, + build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES) + ); + assert_eq!( + season_details_modal.season_details_tabs.tabs[1].contextual_help, + Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) + ); + + assert_str_eq!( + season_details_modal.season_details_tabs.tabs[2].title, + "Manual Search" + ); + assert_eq!( + season_details_modal.season_details_tabs.tabs[2].route, + ActiveSonarrBlock::ManualSeasonSearch.into() + ); + assert_str_eq!( + season_details_modal.season_details_tabs.tabs[2].help, + build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES) + ); + assert_eq!( + season_details_modal.season_details_tabs.tabs[2].contextual_help, + Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) + ); + } } diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index 82b7198..04bab77 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -2,16 +2,29 @@ use bimap::BiMap; use chrono::{DateTime, Utc}; use strum::EnumIter; -use crate::models::{ - servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}, - servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder}, - sonarr_models::{ - AddSeriesSearchResult, BlocklistItem, DownloadRecord, IndexerSettings, Season, Series, - SonarrHistoryItem, SonarrTask, +use crate::{ + app::{ + context_clues::{ + build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + }, + sonarr::sonarr_context_clues::{ + HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, + SERIES_HISTORY_CONTEXT_CLUES, + }, }, - stateful_list::StatefulList, - stateful_table::StatefulTable, - HorizontallyScrollableText, Route, ScrollableText, + models::{ + servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem}, + servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder}, + sonarr_models::{ + AddSeriesSearchResult, BlocklistItem, DownloadRecord, IndexerSettings, Season, Series, + SonarrHistoryItem, SonarrTask, + }, + stateful_list::StatefulList, + stateful_table::StatefulTable, + BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState, + }, + network::sonarr_network::SonarrEvent, }; use super::modals::{AddSeriesModal, EditSeriesModal, SeasonDetailsModal}; @@ -20,7 +33,11 @@ use super::modals::{AddSeriesModal, EditSeriesModal, SeasonDetailsModal}; #[path = "sonarr_data_tests.rs"] mod sonarr_data_tests; -pub struct SonarrData { +#[cfg(test)] +#[path = "sonarr_test_utils.rs"] +pub mod sonarr_test_utils; + +pub struct SonarrData<'a> { pub add_list_exclusion: bool, pub add_searched_series: Option>, pub add_series_modal: Option, @@ -36,16 +53,22 @@ pub struct SonarrData { pub indexers: StatefulTable, pub indexer_settings: Option, pub indexer_test_all_results: Option>, - pub indexer_test_error: Option, + pub indexer_test_errors: Option, pub language_profiles_map: BiMap, pub logs: StatefulList, + pub log_details: StatefulList, + pub main_tabs: TabState, + pub prompt_confirm: bool, + pub prompt_confirm_action: Option, pub quality_profile_map: BiMap, pub queued_events: StatefulTable, pub root_folders: StatefulTable, pub seasons: StatefulTable, pub season_details_modal: Option, + pub selected_block: BlockSelectionState<'a, ActiveSonarrBlock>, pub series: StatefulTable, pub series_history: Option>, + pub series_info_tabs: TabState, pub start_time: DateTime, pub tags_map: BiMap, pub tasks: StatefulTable, @@ -53,15 +76,21 @@ pub struct SonarrData { pub version: String, } -impl SonarrData { +impl<'a> SonarrData<'a> { pub fn reset_delete_series_preferences(&mut self) { self.delete_series_files = false; self.add_list_exclusion = false; } + + pub fn reset_series_info_tabs(&mut self) { + self.series_history = None; + self.seasons = StatefulTable::default(); + self.series_info_tabs.index = 0; + } } -impl Default for SonarrData { - fn default() -> SonarrData { +impl<'a> Default for SonarrData<'a> { + fn default() -> SonarrData<'a> { SonarrData { add_list_exclusion: false, add_searched_series: None, @@ -77,15 +106,19 @@ impl Default for SonarrData { history: StatefulTable::default(), indexers: StatefulTable::default(), indexer_settings: None, - indexer_test_error: None, + indexer_test_errors: None, indexer_test_all_results: None, language_profiles_map: BiMap::new(), logs: StatefulList::default(), + log_details: StatefulList::default(), + prompt_confirm: false, + prompt_confirm_action: None, quality_profile_map: BiMap::new(), queued_events: StatefulTable::default(), root_folders: StatefulTable::default(), seasons: StatefulTable::default(), season_details_modal: None, + selected_block: BlockSelectionState::default(), series: StatefulTable::default(), series_history: None, start_time: DateTime::default(), @@ -93,6 +126,64 @@ impl Default for SonarrData { tasks: StatefulTable::default(), updates: ScrollableText::default(), version: String::new(), + main_tabs: TabState::new(vec![ + TabRoute { + title: "Library", + route: ActiveSonarrBlock::Series.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&SERIES_CONTEXT_CLUES)), + }, + TabRoute { + title: "Downloads", + route: ActiveSonarrBlock::Downloads.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)), + }, + TabRoute { + title: "Blocklist", + route: ActiveSonarrBlock::Blocklist.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)), + }, + TabRoute { + title: "History", + route: ActiveSonarrBlock::History.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)), + }, + TabRoute { + title: "Root Folders", + route: ActiveSonarrBlock::RootFolders.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)), + }, + TabRoute { + title: "Indexers", + route: ActiveSonarrBlock::Indexers.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)), + }, + TabRoute { + title: "System", + route: ActiveSonarrBlock::System.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)), + }, + ]), + series_info_tabs: TabState::new(vec![ + TabRoute { + title: "Seasons", + route: ActiveSonarrBlock::SeriesDetails.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&SERIES_DETAILS_CONTEXT_CLUES)), + }, + TabRoute { + title: "History", + route: ActiveSonarrBlock::SeriesHistory.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)), + }, + ]), } } } @@ -132,6 +223,16 @@ pub enum ActiveSonarrBlock { DeleteSeriesToggleDeleteFile, Downloads, EditIndexerPrompt, + EditIndexerConfirmPrompt, + EditIndexerApiKeyInput, + EditIndexerNameInput, + EditIndexerSeedRatioInput, + EditIndexerToggleEnableRss, + EditIndexerToggleEnableAutomaticSearch, + EditIndexerToggleEnableInteractiveSearch, + EditIndexerUrlInput, + EditIndexerPriorityInput, + EditIndexerTagsInput, EditSeriesPrompt, EditSeriesConfirmPrompt, EditSeriesPathInput, @@ -144,6 +245,7 @@ pub enum ActiveSonarrBlock { EpisodeDetails, EpisodeFile, EpisodeHistory, + EpisodeHistoryDetails, EpisodesSortPrompt, FilterEpisodes, FilterEpisodesError, @@ -153,8 +255,10 @@ pub enum ActiveSonarrBlock { FilterSeriesError, FilterSeriesHistory, FilterSeriesHistoryError, + FilterSeasonHistory, + FilterSeasonHistoryError, History, - HistoryDetails, + HistoryItemDetails, HistorySortPrompt, Indexers, IndexerSettingsConfirmPrompt, @@ -168,8 +272,6 @@ pub enum ActiveSonarrBlock { ManualSeasonSearch, ManualSeasonSearchConfirmPrompt, ManualSeasonSearchSortPrompt, - MarkHistoryItemAsFailedConfirmPrompt, - MarkHistoryItemAsFailedPrompt, RootFolders, SearchEpisodes, SearchEpisodesError, @@ -181,12 +283,17 @@ pub enum ActiveSonarrBlock { SearchSeriesError, SearchSeriesHistory, SearchSeriesHistoryError, + SearchSeasonHistory, + SearchSeasonHistoryError, SeasonDetails, SeasonHistory, + SeasonHistoryDetails, + SeasonHistorySortPrompt, #[default] Series, SeriesDetails, SeriesHistory, + SeriesHistoryDetails, SeriesHistorySortPrompt, SeriesSortPrompt, System, @@ -199,8 +306,251 @@ pub enum ActiveSonarrBlock { TestIndexer, UpdateAllSeriesPrompt, UpdateAndScanSeriesPrompt, + UpdateDownloadsPrompt, } +pub static LIBRARY_BLOCKS: [ActiveSonarrBlock; 7] = [ + ActiveSonarrBlock::Series, + ActiveSonarrBlock::SeriesSortPrompt, + ActiveSonarrBlock::SearchSeries, + ActiveSonarrBlock::SearchSeriesError, + ActiveSonarrBlock::FilterSeries, + ActiveSonarrBlock::FilterSeriesError, + ActiveSonarrBlock::UpdateAllSeriesPrompt, +]; + +pub static SERIES_DETAILS_BLOCKS: [ActiveSonarrBlock; 12] = [ + ActiveSonarrBlock::SeriesDetails, + ActiveSonarrBlock::SeriesHistory, + ActiveSonarrBlock::SearchSeason, + ActiveSonarrBlock::SearchSeasonError, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt, + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + ActiveSonarrBlock::SearchSeriesHistory, + ActiveSonarrBlock::SearchSeriesHistoryError, + ActiveSonarrBlock::FilterSeriesHistory, + ActiveSonarrBlock::FilterSeriesHistoryError, + ActiveSonarrBlock::SeriesHistorySortPrompt, + ActiveSonarrBlock::SeriesHistoryDetails, +]; + +pub static SEASON_DETAILS_BLOCKS: [ActiveSonarrBlock; 15] = [ + ActiveSonarrBlock::SeasonDetails, + ActiveSonarrBlock::SeasonHistory, + ActiveSonarrBlock::SearchEpisodes, + ActiveSonarrBlock::SearchEpisodesError, + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt, + ActiveSonarrBlock::SearchSeasonHistory, + ActiveSonarrBlock::SearchSeasonHistoryError, + ActiveSonarrBlock::FilterSeasonHistory, + ActiveSonarrBlock::FilterSeasonHistoryError, + ActiveSonarrBlock::SeasonHistorySortPrompt, + ActiveSonarrBlock::SeasonHistoryDetails, + ActiveSonarrBlock::ManualSeasonSearch, + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, + ActiveSonarrBlock::ManualSeasonSearchSortPrompt, + ActiveSonarrBlock::DeleteEpisodeFilePrompt, +]; + +pub static EPISODE_DETAILS_BLOCKS: [ActiveSonarrBlock; 8] = [ + ActiveSonarrBlock::EpisodeDetails, + ActiveSonarrBlock::EpisodeHistory, + ActiveSonarrBlock::EpisodeHistoryDetails, + ActiveSonarrBlock::EpisodeFile, + ActiveSonarrBlock::ManualEpisodeSearch, + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt, + ActiveSonarrBlock::ManualEpisodeSearchSortPrompt, + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt, +]; + +pub static ADD_SERIES_BLOCKS: [ActiveSonarrBlock; 13] = [ + ActiveSonarrBlock::AddSeriesAlreadyInLibrary, + ActiveSonarrBlock::AddSeriesConfirmPrompt, + ActiveSonarrBlock::AddSeriesEmptySearchResults, + ActiveSonarrBlock::AddSeriesPrompt, + ActiveSonarrBlock::AddSeriesSearchInput, + ActiveSonarrBlock::AddSeriesSearchResults, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + ActiveSonarrBlock::AddSeriesSelectMonitor, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + ActiveSonarrBlock::AddSeriesSelectRootFolder, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + ActiveSonarrBlock::AddSeriesTagsInput, + ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder, +]; + +pub const ADD_SERIES_SELECTION_BLOCKS: &[&[ActiveSonarrBlock]] = &[ + &[ActiveSonarrBlock::AddSeriesSelectRootFolder], + &[ActiveSonarrBlock::AddSeriesSelectMonitor], + &[ActiveSonarrBlock::AddSeriesSelectQualityProfile], + &[ActiveSonarrBlock::AddSeriesSelectLanguageProfile], + &[ActiveSonarrBlock::AddSeriesSelectSeriesType], + &[ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder], + &[ActiveSonarrBlock::AddSeriesTagsInput], + &[ActiveSonarrBlock::AddSeriesConfirmPrompt], +]; + +pub static BLOCKLIST_BLOCKS: [ActiveSonarrBlock; 5] = [ + ActiveSonarrBlock::Blocklist, + ActiveSonarrBlock::BlocklistItemDetails, + ActiveSonarrBlock::DeleteBlocklistItemPrompt, + ActiveSonarrBlock::BlocklistClearAllItemsPrompt, + ActiveSonarrBlock::BlocklistSortPrompt, +]; + +pub static EDIT_SERIES_BLOCKS: [ActiveSonarrBlock; 9] = [ + ActiveSonarrBlock::EditSeriesPrompt, + ActiveSonarrBlock::EditSeriesConfirmPrompt, + ActiveSonarrBlock::EditSeriesPathInput, + ActiveSonarrBlock::EditSeriesSelectSeriesType, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile, + ActiveSonarrBlock::EditSeriesTagsInput, + ActiveSonarrBlock::EditSeriesToggleMonitored, + ActiveSonarrBlock::EditSeriesToggleSeasonFolder, +]; + +pub static EDIT_SERIES_SELECTION_BLOCKS: &[&[ActiveSonarrBlock]] = &[ + &[ActiveSonarrBlock::EditSeriesToggleMonitored], + &[ActiveSonarrBlock::EditSeriesToggleSeasonFolder], + &[ActiveSonarrBlock::EditSeriesSelectQualityProfile], + &[ActiveSonarrBlock::EditSeriesSelectLanguageProfile], + &[ActiveSonarrBlock::EditSeriesSelectSeriesType], + &[ActiveSonarrBlock::EditSeriesPathInput], + &[ActiveSonarrBlock::EditSeriesTagsInput], + &[ActiveSonarrBlock::EditSeriesConfirmPrompt], +]; + +pub static DOWNLOADS_BLOCKS: [ActiveSonarrBlock; 3] = [ + ActiveSonarrBlock::Downloads, + ActiveSonarrBlock::DeleteDownloadPrompt, + ActiveSonarrBlock::UpdateDownloadsPrompt, +]; + +pub static DELETE_SERIES_BLOCKS: [ActiveSonarrBlock; 4] = [ + ActiveSonarrBlock::DeleteSeriesPrompt, + ActiveSonarrBlock::DeleteSeriesConfirmPrompt, + ActiveSonarrBlock::DeleteSeriesToggleDeleteFile, + ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion, +]; + +pub const DELETE_SERIES_SELECTION_BLOCKS: &[&[ActiveSonarrBlock]] = &[ + &[ActiveSonarrBlock::DeleteSeriesToggleDeleteFile], + &[ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion], + &[ActiveSonarrBlock::DeleteSeriesConfirmPrompt], +]; + +pub static EDIT_INDEXER_BLOCKS: [ActiveSonarrBlock; 11] = [ + ActiveSonarrBlock::EditIndexerPrompt, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ActiveSonarrBlock::EditIndexerApiKeyInput, + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + ActiveSonarrBlock::EditIndexerToggleEnableRss, + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveSonarrBlock::EditIndexerPriorityInput, + ActiveSonarrBlock::EditIndexerUrlInput, + ActiveSonarrBlock::EditIndexerTagsInput, +]; + +pub const EDIT_INDEXER_TORRENT_SELECTION_BLOCKS: &[&[ActiveSonarrBlock]] = &[ + &[ + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveSonarrBlock::EditIndexerToggleEnableRss, + ActiveSonarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + ], + &[ + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveSonarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveSonarrBlock::EditIndexerPriorityInput, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ], + &[ + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ], +]; + +pub const EDIT_INDEXER_NZB_SELECTION_BLOCKS: &[&[ActiveSonarrBlock]] = &[ + &[ + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerUrlInput, + ], + &[ + ActiveSonarrBlock::EditIndexerToggleEnableRss, + ActiveSonarrBlock::EditIndexerApiKeyInput, + ], + &[ + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveSonarrBlock::EditIndexerTagsInput, + ], + &[ + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveSonarrBlock::EditIndexerPriorityInput, + ], + &[ + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ], +]; + +pub static INDEXER_SETTINGS_BLOCKS: [ActiveSonarrBlock; 6] = [ + ActiveSonarrBlock::AllIndexerSettingsPrompt, + ActiveSonarrBlock::IndexerSettingsConfirmPrompt, + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput, + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput, + ActiveSonarrBlock::IndexerSettingsRetentionInput, + ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput, +]; + +pub const INDEXER_SETTINGS_SELECTION_BLOCKS: &[&[ActiveSonarrBlock]] = &[ + &[ActiveSonarrBlock::IndexerSettingsMinimumAgeInput], + &[ActiveSonarrBlock::IndexerSettingsRetentionInput], + &[ActiveSonarrBlock::IndexerSettingsMaximumSizeInput], + &[ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput], + &[ActiveSonarrBlock::IndexerSettingsConfirmPrompt], +]; + +pub static HISTORY_BLOCKS: [ActiveSonarrBlock; 7] = [ + ActiveSonarrBlock::History, + ActiveSonarrBlock::HistoryItemDetails, + ActiveSonarrBlock::HistorySortPrompt, + ActiveSonarrBlock::FilterHistory, + ActiveSonarrBlock::FilterHistoryError, + ActiveSonarrBlock::SearchHistory, + ActiveSonarrBlock::SearchHistoryError, +]; + +pub static ROOT_FOLDERS_BLOCKS: [ActiveSonarrBlock; 3] = [ + ActiveSonarrBlock::RootFolders, + ActiveSonarrBlock::AddRootFolderPrompt, + ActiveSonarrBlock::DeleteRootFolderPrompt, +]; + +pub static INDEXERS_BLOCKS: [ActiveSonarrBlock; 3] = [ + ActiveSonarrBlock::DeleteIndexerPrompt, + ActiveSonarrBlock::Indexers, + ActiveSonarrBlock::TestIndexer, +]; + +pub static SYSTEM_DETAILS_BLOCKS: [ActiveSonarrBlock; 5] = [ + ActiveSonarrBlock::SystemLogs, + ActiveSonarrBlock::SystemQueuedEvents, + ActiveSonarrBlock::SystemTasks, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt, + ActiveSonarrBlock::SystemUpdates, +]; + impl From for Route { fn from(active_sonarr_block: ActiveSonarrBlock) -> Route { Route::Sonarr(active_sonarr_block, None) diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index de9b25a..36d18bd 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -2,10 +2,25 @@ mod tests { mod sonarr_data_tests { use chrono::{DateTime, Utc}; + use pretty_assertions::{assert_eq, assert_str_eq}; - use crate::models::{ - servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}, - Route, + use crate::app::sonarr::sonarr_context_clues::SERIES_HISTORY_CONTEXT_CLUES; + use crate::models::sonarr_models::{Season, SonarrHistoryItem}; + use crate::models::stateful_table::StatefulTable; + use crate::{ + app::{ + context_clues::{ + build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + }, + sonarr::sonarr_context_clues::{ + HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, + }, + }, + models::{ + servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}, + BlockSelectionState, Route, + }, }; #[test] @@ -44,6 +59,24 @@ mod tests { assert!(!sonarr_data.add_list_exclusion); } + #[test] + fn test_reset_series_info_tabs() { + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + let mut sonarr_data = SonarrData { + series_history: Some(series_history), + ..SonarrData::default() + }; + sonarr_data.seasons.set_items(vec![Season::default()]); + sonarr_data.series_info_tabs.index = 1; + + sonarr_data.reset_series_info_tabs(); + + assert!(sonarr_data.series_history.is_none()); + assert!(sonarr_data.seasons.is_empty()); + assert_eq!(sonarr_data.series_info_tabs.index, 0); + } + #[test] fn test_sonarr_data_defaults() { let sonarr_data = SonarrData::default(); @@ -62,15 +95,19 @@ mod tests { assert!(sonarr_data.history.is_empty()); assert!(sonarr_data.indexers.is_empty()); assert!(sonarr_data.indexer_settings.is_none()); - assert!(sonarr_data.indexer_test_error.is_none()); + assert!(sonarr_data.indexer_test_errors.is_none()); assert!(sonarr_data.indexer_test_all_results.is_none()); assert!(sonarr_data.language_profiles_map.is_empty()); assert!(sonarr_data.logs.is_empty()); + assert!(sonarr_data.log_details.is_empty()); + assert!(!sonarr_data.prompt_confirm); + assert!(sonarr_data.prompt_confirm_action.is_none()); assert!(sonarr_data.quality_profile_map.is_empty()); assert!(sonarr_data.queued_events.is_empty()); assert!(sonarr_data.root_folders.is_empty()); assert!(sonarr_data.seasons.is_empty()); assert!(sonarr_data.season_details_modal.is_none()); + assert_eq!(sonarr_data.selected_block, BlockSelectionState::default()); assert!(sonarr_data.series.is_empty()); assert!(sonarr_data.series_history.is_none()); assert_eq!(sonarr_data.start_time, >::default()); @@ -78,6 +115,528 @@ mod tests { assert!(sonarr_data.tasks.is_empty()); assert!(sonarr_data.updates.is_empty()); assert!(sonarr_data.version.is_empty()); + + assert_eq!(sonarr_data.main_tabs.tabs.len(), 7); + + assert_str_eq!(sonarr_data.main_tabs.tabs[0].title, "Library"); + assert_eq!( + sonarr_data.main_tabs.tabs[0].route, + ActiveSonarrBlock::Series.into() + ); + assert!(sonarr_data.main_tabs.tabs[0].help.is_empty()); + assert_eq!( + sonarr_data.main_tabs.tabs[0].contextual_help, + Some(build_context_clue_string(&SERIES_CONTEXT_CLUES)) + ); + + assert_str_eq!(sonarr_data.main_tabs.tabs[1].title, "Downloads"); + assert_eq!( + sonarr_data.main_tabs.tabs[1].route, + ActiveSonarrBlock::Downloads.into() + ); + assert!(sonarr_data.main_tabs.tabs[1].help.is_empty()); + assert_eq!( + sonarr_data.main_tabs.tabs[1].contextual_help, + Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)) + ); + + assert_str_eq!(sonarr_data.main_tabs.tabs[2].title, "Blocklist"); + assert_eq!( + sonarr_data.main_tabs.tabs[2].route, + ActiveSonarrBlock::Blocklist.into() + ); + assert!(sonarr_data.main_tabs.tabs[2].help.is_empty()); + assert_eq!( + sonarr_data.main_tabs.tabs[2].contextual_help, + Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)) + ); + + assert_str_eq!(sonarr_data.main_tabs.tabs[3].title, "History"); + assert_eq!( + sonarr_data.main_tabs.tabs[3].route, + ActiveSonarrBlock::History.into() + ); + assert!(sonarr_data.main_tabs.tabs[3].help.is_empty()); + assert_eq!( + sonarr_data.main_tabs.tabs[3].contextual_help, + Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)) + ); + + assert_str_eq!(sonarr_data.main_tabs.tabs[4].title, "Root Folders"); + assert_eq!( + sonarr_data.main_tabs.tabs[4].route, + ActiveSonarrBlock::RootFolders.into() + ); + assert!(sonarr_data.main_tabs.tabs[4].help.is_empty()); + assert_eq!( + sonarr_data.main_tabs.tabs[4].contextual_help, + Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)) + ); + + assert_str_eq!(sonarr_data.main_tabs.tabs[5].title, "Indexers"); + assert_eq!( + sonarr_data.main_tabs.tabs[5].route, + ActiveSonarrBlock::Indexers.into() + ); + assert!(sonarr_data.main_tabs.tabs[5].help.is_empty()); + assert_eq!( + sonarr_data.main_tabs.tabs[5].contextual_help, + Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)) + ); + + assert_str_eq!(sonarr_data.main_tabs.tabs[6].title, "System"); + assert_eq!( + sonarr_data.main_tabs.tabs[6].route, + ActiveSonarrBlock::System.into() + ); + assert!(sonarr_data.main_tabs.tabs[6].help.is_empty()); + assert_eq!( + sonarr_data.main_tabs.tabs[6].contextual_help, + Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)) + ); + + assert_eq!(sonarr_data.series_info_tabs.tabs.len(), 2); + + assert_str_eq!(sonarr_data.series_info_tabs.tabs[0].title, "Seasons"); + assert_eq!( + sonarr_data.series_info_tabs.tabs[0].route, + ActiveSonarrBlock::SeriesDetails.into() + ); + assert!(sonarr_data.series_info_tabs.tabs[0].help.is_empty()); + assert_eq!( + sonarr_data.series_info_tabs.tabs[0].contextual_help, + Some(build_context_clue_string(&SERIES_DETAILS_CONTEXT_CLUES)) + ); + + assert_str_eq!(sonarr_data.series_info_tabs.tabs[1].title, "History"); + assert_eq!( + sonarr_data.series_info_tabs.tabs[1].route, + ActiveSonarrBlock::SeriesHistory.into() + ); + assert!(sonarr_data.series_info_tabs.tabs[1].help.is_empty()); + assert_eq!( + sonarr_data.series_info_tabs.tabs[1].contextual_help, + Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)) + ); + } + } + + mod active_sonarr_block_tests { + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, BLOCKLIST_BLOCKS, + DELETE_SERIES_BLOCKS, DELETE_SERIES_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_INDEXER_BLOCKS, + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDIT_SERIES_BLOCKS, + EDIT_SERIES_SELECTION_BLOCKS, EPISODE_DETAILS_BLOCKS, HISTORY_BLOCKS, INDEXERS_BLOCKS, + INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS, + ROOT_FOLDERS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS, SYSTEM_DETAILS_BLOCKS, + }; + + #[test] + fn test_library_blocks_contents() { + assert_eq!(LIBRARY_BLOCKS.len(), 7); + assert!(LIBRARY_BLOCKS.contains(&ActiveSonarrBlock::Series)); + assert!(LIBRARY_BLOCKS.contains(&ActiveSonarrBlock::SeriesSortPrompt)); + assert!(LIBRARY_BLOCKS.contains(&ActiveSonarrBlock::SearchSeries)); + assert!(LIBRARY_BLOCKS.contains(&ActiveSonarrBlock::SearchSeriesError)); + assert!(LIBRARY_BLOCKS.contains(&ActiveSonarrBlock::FilterSeries)); + assert!(LIBRARY_BLOCKS.contains(&ActiveSonarrBlock::FilterSeriesError)); + assert!(LIBRARY_BLOCKS.contains(&ActiveSonarrBlock::UpdateAllSeriesPrompt)); + } + + #[test] + fn test_add_series_blocks_contents() { + assert_eq!(ADD_SERIES_BLOCKS.len(), 13); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesAlreadyInLibrary)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesConfirmPrompt)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesEmptySearchResults)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesPrompt)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesSearchInput)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesSearchResults)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesSelectLanguageProfile)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesSelectMonitor)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesSelectQualityProfile)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesSelectRootFolder)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesSelectSeriesType)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesTagsInput)); + assert!(ADD_SERIES_BLOCKS.contains(&ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder)); + } + + #[test] + fn test_add_series_selection_blocks_ordering() { + let mut add_series_block_iter = ADD_SERIES_SELECTION_BLOCKS.iter(); + + assert_eq!( + add_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::AddSeriesSelectRootFolder] + ); + assert_eq!( + add_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::AddSeriesSelectMonitor] + ); + assert_eq!( + add_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::AddSeriesSelectQualityProfile] + ); + assert_eq!( + add_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::AddSeriesSelectLanguageProfile] + ); + assert_eq!( + add_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::AddSeriesSelectSeriesType] + ); + assert_eq!( + add_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder] + ); + assert_eq!( + add_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::AddSeriesTagsInput] + ); + assert_eq!( + add_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::AddSeriesConfirmPrompt] + ); + assert_eq!(add_series_block_iter.next(), None); + } + + #[test] + fn test_blocklist_blocks_contents() { + assert_eq!(BLOCKLIST_BLOCKS.len(), 5); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveSonarrBlock::Blocklist)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveSonarrBlock::BlocklistItemDetails)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveSonarrBlock::DeleteBlocklistItemPrompt)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveSonarrBlock::BlocklistClearAllItemsPrompt)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveSonarrBlock::BlocklistSortPrompt)); + } + + #[test] + fn test_edit_movie_blocks_contents() { + assert_eq!(EDIT_SERIES_BLOCKS.len(), 9); + assert!(EDIT_SERIES_BLOCKS.contains(&ActiveSonarrBlock::EditSeriesPrompt)); + assert!(EDIT_SERIES_BLOCKS.contains(&ActiveSonarrBlock::EditSeriesConfirmPrompt)); + assert!(EDIT_SERIES_BLOCKS.contains(&ActiveSonarrBlock::EditSeriesPathInput)); + assert!(EDIT_SERIES_BLOCKS.contains(&ActiveSonarrBlock::EditSeriesSelectSeriesType)); + assert!(EDIT_SERIES_BLOCKS.contains(&ActiveSonarrBlock::EditSeriesSelectQualityProfile)); + assert!(EDIT_SERIES_BLOCKS.contains(&ActiveSonarrBlock::EditSeriesSelectLanguageProfile)); + assert!(EDIT_SERIES_BLOCKS.contains(&ActiveSonarrBlock::EditSeriesTagsInput)); + assert!(EDIT_SERIES_BLOCKS.contains(&ActiveSonarrBlock::EditSeriesToggleMonitored)); + assert!(EDIT_SERIES_BLOCKS.contains(&ActiveSonarrBlock::EditSeriesToggleSeasonFolder)); + } + + #[test] + fn test_edit_series_selection_blocks_ordering() { + let mut edit_series_block_iter = EDIT_SERIES_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::EditSeriesToggleMonitored] + ); + assert_eq!( + edit_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::EditSeriesToggleSeasonFolder] + ); + assert_eq!( + edit_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::EditSeriesSelectQualityProfile] + ); + assert_eq!( + edit_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::EditSeriesSelectLanguageProfile] + ); + assert_eq!( + edit_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::EditSeriesSelectSeriesType] + ); + assert_eq!( + edit_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::EditSeriesPathInput] + ); + assert_eq!( + edit_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::EditSeriesTagsInput] + ); + assert_eq!( + edit_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::EditSeriesConfirmPrompt] + ); + assert_eq!(edit_series_block_iter.next(), None); + } + + #[test] + fn test_downloads_blocks_contents() { + assert_eq!(DOWNLOADS_BLOCKS.len(), 3); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveSonarrBlock::Downloads)); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveSonarrBlock::DeleteDownloadPrompt)); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveSonarrBlock::UpdateDownloadsPrompt)); + } + + #[test] + fn test_delete_series_blocks_contents() { + assert_eq!(DELETE_SERIES_BLOCKS.len(), 4); + assert!(DELETE_SERIES_BLOCKS.contains(&ActiveSonarrBlock::DeleteSeriesPrompt)); + assert!(DELETE_SERIES_BLOCKS.contains(&ActiveSonarrBlock::DeleteSeriesConfirmPrompt)); + assert!(DELETE_SERIES_BLOCKS.contains(&ActiveSonarrBlock::DeleteSeriesToggleDeleteFile)); + assert!(DELETE_SERIES_BLOCKS.contains(&ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion)); + } + + #[test] + fn test_delete_series_selection_blocks_ordering() { + let mut delete_series_block_iter = DELETE_SERIES_SELECTION_BLOCKS.iter(); + + assert_eq!( + delete_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::DeleteSeriesToggleDeleteFile] + ); + assert_eq!( + delete_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion] + ); + assert_eq!( + delete_series_block_iter.next().unwrap(), + &[ActiveSonarrBlock::DeleteSeriesConfirmPrompt] + ); + assert_eq!(delete_series_block_iter.next(), None); + } + + #[test] + fn test_edit_indexer_blocks_contents() { + assert_eq!(EDIT_INDEXER_BLOCKS.len(), 11); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerConfirmPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerApiKeyInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerNameInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerSeedRatioInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerToggleEnableRss)); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch) + ); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch) + ); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerUrlInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerTagsInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveSonarrBlock::EditIndexerPriorityInput)); + } + + #[test] + fn test_edit_indexer_nzb_selection_blocks_ordering() { + let mut edit_indexer_nzb_selection_block_iter = EDIT_INDEXER_NZB_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerUrlInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerToggleEnableRss, + ActiveSonarrBlock::EditIndexerApiKeyInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveSonarrBlock::EditIndexerTagsInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveSonarrBlock::EditIndexerPriorityInput, + ] + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ] + ); + assert_eq!(edit_indexer_nzb_selection_block_iter.next(), None); + } + + #[test] + fn test_edit_indexer_torrent_selection_blocks_ordering() { + let mut edit_indexer_torrent_selection_block_iter = + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerUrlInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerToggleEnableRss, + ActiveSonarrBlock::EditIndexerApiKeyInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveSonarrBlock::EditIndexerTagsInput, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerPriorityInput, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ] + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &[ + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ] + ); + assert_eq!(edit_indexer_torrent_selection_block_iter.next(), None); + } + + #[test] + fn test_indexer_settings_blocks_contents() { + assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 6); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveSonarrBlock::AllIndexerSettingsPrompt)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveSonarrBlock::IndexerSettingsConfirmPrompt)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveSonarrBlock::IndexerSettingsMaximumSizeInput)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveSonarrBlock::IndexerSettingsMinimumAgeInput)); + assert!(INDEXER_SETTINGS_BLOCKS.contains(&ActiveSonarrBlock::IndexerSettingsRetentionInput)); + assert!( + INDEXER_SETTINGS_BLOCKS.contains(&ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput) + ); + } + + #[test] + fn test_indexer_settings_selection_blocks_ordering() { + let mut indexer_settings_block_iter = INDEXER_SETTINGS_SELECTION_BLOCKS.iter(); + + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveSonarrBlock::IndexerSettingsMinimumAgeInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveSonarrBlock::IndexerSettingsRetentionInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveSonarrBlock::IndexerSettingsMaximumSizeInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput,] + ); + assert_eq!( + indexer_settings_block_iter.next().unwrap(), + &[ActiveSonarrBlock::IndexerSettingsConfirmPrompt,] + ); + assert_eq!(indexer_settings_block_iter.next(), None); + } + + #[test] + fn test_history_blocks_contents() { + assert_eq!(HISTORY_BLOCKS.len(), 7); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::History)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::HistoryItemDetails)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::HistorySortPrompt)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::FilterHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::FilterHistoryError)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::SearchHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::SearchHistoryError)); + } + + #[test] + fn test_root_folders_blocks_contents() { + assert_eq!(ROOT_FOLDERS_BLOCKS.len(), 3); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveSonarrBlock::RootFolders)); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveSonarrBlock::AddRootFolderPrompt)); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveSonarrBlock::DeleteRootFolderPrompt)); + } + + #[test] + fn test_indexers_blocks_contents() { + assert_eq!(INDEXERS_BLOCKS.len(), 3); + assert!(INDEXERS_BLOCKS.contains(&ActiveSonarrBlock::DeleteIndexerPrompt)); + assert!(INDEXERS_BLOCKS.contains(&ActiveSonarrBlock::Indexers)); + assert!(INDEXERS_BLOCKS.contains(&ActiveSonarrBlock::TestIndexer)); + } + + #[test] + fn test_system_details_blocks_contents() { + assert_eq!(SYSTEM_DETAILS_BLOCKS.len(), 5); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SystemLogs)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SystemQueuedEvents)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SystemTasks)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SystemTaskStartConfirmPrompt)); + assert!(SYSTEM_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SystemUpdates)); + } + + #[test] + fn test_series_details_blocks_contents() { + assert_eq!(SERIES_DETAILS_BLOCKS.len(), 12); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesDetails)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesHistory)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeason)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeasonError)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::UpdateAndScanSeriesPrompt)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::AutomaticallySearchSeriesPrompt)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeriesHistory)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeriesHistoryError)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::FilterSeriesHistory)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::FilterSeriesHistoryError)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesHistorySortPrompt)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesHistoryDetails)); + } + + #[test] + fn test_season_details_blocks_contents() { + assert_eq!(SEASON_DETAILS_BLOCKS.len(), 15); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeasonDetails)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeasonHistory)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchEpisodes)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchEpisodesError)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::AutomaticallySearchSeasonPrompt)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeasonHistory)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeasonHistoryError)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::FilterSeasonHistory)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::FilterSeasonHistoryError)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeasonHistorySortPrompt)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeasonHistoryDetails)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::ManualSeasonSearch)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::ManualSeasonSearchSortPrompt)); + assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::DeleteEpisodeFilePrompt)); + } + + #[test] + fn test_episode_details_blocks_contents() { + assert_eq!(EPISODE_DETAILS_BLOCKS.len(), 8); + assert!(EPISODE_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::EpisodeDetails)); + assert!(EPISODE_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::EpisodeHistory)); + assert!(EPISODE_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::EpisodeHistoryDetails)); + assert!(EPISODE_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::EpisodeFile)); + assert!(EPISODE_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::ManualEpisodeSearch)); + assert!(EPISODE_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::ManualEpisodeSearchSortPrompt)); + assert!(EPISODE_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt)); + assert!(EPISODE_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::AutomaticallySearchEpisodePrompt)); } } } diff --git a/src/models/servarr_data/sonarr/sonarr_test_utils.rs b/src/models/servarr_data/sonarr/sonarr_test_utils.rs new file mode 100644 index 0000000..2dd552a --- /dev/null +++ b/src/models/servarr_data/sonarr/sonarr_test_utils.rs @@ -0,0 +1,77 @@ +#[cfg(test)] +pub mod utils { + use crate::models::servarr_models::{Indexer, RootFolder}; + use crate::models::sonarr_models::{BlocklistItem, Series}; + use crate::models::{ + servarr_data::sonarr::{ + modals::{EpisodeDetailsModal, SeasonDetailsModal}, + sonarr_data::SonarrData, + }, + sonarr_models::{AddSeriesSearchResult, Episode, Season, SonarrHistoryItem, SonarrRelease}, + stateful_table::StatefulTable, + HorizontallyScrollableText, ScrollableText, + }; + + pub fn create_test_sonarr_data<'a>() -> SonarrData<'a> { + let mut episode_details_modal = EpisodeDetailsModal { + episode_details: ScrollableText::with_string("test episode details".to_owned()), + ..EpisodeDetailsModal::default() + }; + episode_details_modal + .episode_history + .set_items(vec![SonarrHistoryItem::default()]); + episode_details_modal + .episode_releases + .set_items(vec![SonarrRelease::default()]); + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal + .episodes + .set_items(vec![Episode::default()]); + season_details_modal + .season_history + .set_items(vec![SonarrHistoryItem::default()]); + season_details_modal + .season_releases + .set_items(vec![SonarrRelease::default()]); + season_details_modal.episode_details_modal = Some(episode_details_modal); + + let mut seasons = StatefulTable::default(); + seasons.set_items(vec![Season::default()]); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + + let mut sonarr_data = SonarrData { + delete_series_files: true, + add_list_exclusion: true, + add_series_search: Some("test search".into()), + edit_root_folder: Some("test path".into()), + seasons, + series_history: Some(series_history), + season_details_modal: Some(season_details_modal), + add_searched_series: Some(StatefulTable::default()), + ..SonarrData::default() + }; + sonarr_data.series.set_items(vec![Series::default()]); + sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + sonarr_data + .blocklist + .set_items(vec![BlocklistItem::default()]); + sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + sonarr_data.indexers.set_items(vec![Indexer::default()]); + sonarr_data.series_info_tabs.index = 1; + sonarr_data + .add_searched_series + .as_mut() + .unwrap() + .set_items(vec![AddSeriesSearchResult::default()]); + sonarr_data + .log_details + .set_items(vec![HorizontallyScrollableText::default()]); + + sonarr_data + } +} diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index d98d9a7..48f55ef 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -78,9 +78,11 @@ pub struct BlocklistItem { pub id: i64, #[serde(deserialize_with = "super::from_i64")] pub series_id: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub series_title: Option, pub episode_ids: Vec, pub source_title: String, - pub language: Language, + pub languages: Vec, pub quality: QualityWrapper, pub date: DateTime, pub protocol: String, @@ -105,7 +107,7 @@ pub struct DeleteSeriesParams { #[serde(rename_all = "camelCase")] pub struct DownloadRecord { pub title: String, - pub status: String, + pub status: DownloadStatus, #[serde(deserialize_with = "super::from_i64")] pub id: i64, #[serde(deserialize_with = "super::from_i64")] @@ -117,11 +119,62 @@ pub struct DownloadRecord { pub output_path: Option, #[serde(default)] pub indexer: String, - pub download_client: String, + pub download_client: Option, } impl Eq for DownloadRecord {} +#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)] +#[serde(rename_all = "camelCase")] +pub enum DownloadStatus { + #[default] + Unknown, + Queued, + Paused, + Downloading, + Completed, + Failed, + Warning, + Delay, + DownloadClientUnavailable, + Fallback, +} + +impl Display for DownloadStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let download_status = match self { + DownloadStatus::Unknown => "unknown", + DownloadStatus::Queued => "queued", + DownloadStatus::Paused => "paused", + DownloadStatus::Downloading => "downloading", + DownloadStatus::Completed => "completed", + DownloadStatus::Failed => "failed", + DownloadStatus::Warning => "warning", + DownloadStatus::Delay => "delay", + DownloadStatus::DownloadClientUnavailable => "downloadClientUnavailable", + DownloadStatus::Fallback => "fallback", + }; + write!(f, "{download_status}") + } +} + +impl<'a> EnumDisplayStyle<'a> for DownloadStatus { + fn to_display_str(self) -> &'a str { + match self { + DownloadStatus::Unknown => "Unknown", + DownloadStatus::Queued => "Queued", + DownloadStatus::Paused => "Paused", + DownloadStatus::Downloading => "Downloading", + DownloadStatus::Completed => "Completed", + DownloadStatus::Failed => "Failed", + DownloadStatus::Warning => "Warning", + DownloadStatus::Delay => "Delay", + DownloadStatus::DownloadClientUnavailable => "Download Client Unavailable", + DownloadStatus::Fallback => "Fallback", + } + } +} + #[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct DownloadsResponse { @@ -157,7 +210,7 @@ pub struct Episode { pub season_number: i64, #[serde(deserialize_with = "super::from_i64")] pub episode_number: i64, - pub title: Option, + pub title: String, pub air_date_utc: Option>, pub overview: Option, pub has_file: bool, @@ -167,18 +220,21 @@ pub struct Episode { impl Display for Episode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.title.as_ref().unwrap_or(&String::new())) + write!(f, "{}", self.title) } } #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct EpisodeFile { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, pub relative_path: String, pub path: String, #[serde(deserialize_with = "super::from_i64")] pub size: i64, - pub language: Language, + pub languages: Vec, + pub quality: QualityWrapper, pub date_added: DateTime, pub media_info: Option, } @@ -223,6 +279,13 @@ pub struct MediaInfo { pub subtitles: Option, } +#[derive(Default, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MonitorEpisodeBody { + pub episode_ids: Vec, + pub monitored: bool, +} + #[derive(Derivative, Serialize, Deserialize, Debug, Clone, PartialEq)] #[derivative(Default)] pub struct Rating { @@ -237,6 +300,8 @@ impl Eq for Rating {} #[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Season { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, #[serde(deserialize_with = "super::from_i64")] pub season_number: i64, pub monitored: bool, @@ -463,6 +528,10 @@ pub struct SonarrHistoryData { pub published_date: Option>, pub message: Option, pub reason: Option, + pub source_path: Option, + pub source_relative_path: Option, + pub path: Option, + pub relative_path: Option, } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] @@ -519,9 +588,9 @@ pub struct SonarrHistoryItem { #[serde(deserialize_with = "super::from_i64")] pub episode_id: i64, pub quality: QualityWrapper, - pub language: Language, + pub languages: Vec, pub date: DateTime, - pub event_type: String, + pub event_type: SonarrHistoryEventType, pub data: SonarrHistoryData, } @@ -619,6 +688,7 @@ pub enum SonarrSerdeable { DiskSpaces(Vec), Episode(Episode), Episodes(Vec), + EpisodeFiles(Vec), HostConfig(HostConfig), IndexerSettings(IndexerSettings), Indexers(Vec), @@ -662,6 +732,7 @@ serde_enum_from!( DiskSpaces(Vec), Episode(Episode), Episodes(Vec), + EpisodeFiles(Vec), HostConfig(HostConfig), IndexerSettings(IndexerSettings), Indexers(Vec), diff --git a/src/models/sonarr_models_tests.rs b/src/models/sonarr_models_tests.rs index 87ac76d..0c1ea46 100644 --- a/src/models/sonarr_models_tests.rs +++ b/src/models/sonarr_models_tests.rs @@ -10,10 +10,10 @@ mod tests { RootFolder, SecurityConfig, Tag, Update, }, sonarr_models::{ - AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse, - Episode, IndexerSettings, Series, SeriesMonitor, SeriesStatus, SeriesType, - SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask, - SonarrTaskName, SystemStatus, + AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadStatus, + DownloadsResponse, Episode, EpisodeFile, IndexerSettings, Series, SeriesMonitor, + SeriesStatus, SeriesType, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, + SonarrSerdeable, SonarrTask, SonarrTaskName, SystemStatus, }, EnumDisplayStyle, Serdeable, }; @@ -21,7 +21,7 @@ mod tests { #[test] fn test_episode_display() { let episode = Episode { - title: Some("Test Title".to_owned()), + title: "Test Title".to_owned(), ..Episode::default() }; @@ -119,6 +119,40 @@ mod tests { assert_str_eq!(SeriesType::Anime.to_display_str(), "Anime"); } + #[test] + fn test_download_status_display() { + assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown"); + assert_str_eq!(DownloadStatus::Queued.to_string(), "queued"); + assert_str_eq!(DownloadStatus::Paused.to_string(), "paused"); + assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading"); + assert_str_eq!(DownloadStatus::Completed.to_string(), "completed"); + assert_str_eq!(DownloadStatus::Failed.to_string(), "failed"); + assert_str_eq!(DownloadStatus::Warning.to_string(), "warning"); + assert_str_eq!(DownloadStatus::Delay.to_string(), "delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_string(), + "downloadClientUnavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback"); + } + + #[test] + fn test_download_status_to_display_str() { + assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown"); + assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued"); + assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused"); + assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading"); + assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed"); + assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed"); + assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning"); + assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_display_str(), + "Download Client Unavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback"); + } + #[test] fn test_sonarr_history_event_type_display() { assert_str_eq!(SonarrHistoryEventType::Unknown.to_string(), "unknown",); @@ -236,6 +270,21 @@ mod tests { assert_eq!(sonarr_serdeable, SonarrSerdeable::Episodes(episodes)); } + #[test] + fn test_sonarr_serdeable_from_episode_files() { + let episode_files = vec![EpisodeFile { + id: 1, + ..EpisodeFile::default() + }]; + + let sonarr_serdeable: SonarrSerdeable = episode_files.clone().into(); + + assert_eq!( + sonarr_serdeable, + SonarrSerdeable::EpisodeFiles(episode_files) + ); + } + #[test] fn test_sonarr_serdeable_from_host_config() { let host_config = HostConfig { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 0c5a8c3..60497af 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -309,7 +309,7 @@ impl<'a, 'b> Network<'a, 'b> { quality_profile_list, .. } = app.data.radarr_data.add_movie_modal.as_ref().unwrap(); - let (tmdb_id, title) = if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() + let (tmdb_id, title) = if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { if active_radarr_block == ActiveRadarrBlock::CollectionDetails { let CollectionMovie { tmdb_id, title, .. } = app @@ -927,9 +927,6 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit indexer body"); let mut detailed_indexer_body: Value = serde_json::from_str(&response).unwrap(); - let priority = detailed_indexer_body["priority"] - .as_i64() - .expect("Unable to deserialize 'priority'"); let ( name, @@ -942,6 +939,9 @@ impl<'a, 'b> Network<'a, 'b> { tags, priority, ) = if let Some(params) = edit_indexer_params { + let priority = detailed_indexer_body["priority"] + .as_i64() + .expect("Unable to deserialize 'priority'"); let seed_ratio_field_option = detailed_indexer_body["fields"] .as_array() .unwrap() @@ -1056,6 +1056,7 @@ impl<'a, 'b> Network<'a, 'b> { url, api_key, seed_ratio, + priority, .. } = app.data.radarr_data.edit_indexer_modal.as_ref().unwrap(); @@ -1068,7 +1069,7 @@ impl<'a, 'b> Network<'a, 'b> { api_key.text.clone(), seed_ratio.text.clone(), tag_ids_vec, - priority, + *priority, ) }; @@ -2101,12 +2102,14 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |test_results, mut app| { if test_results.as_object().is_none() { - app.data.radarr_data.indexer_test_error = Some( + app.data.radarr_data.indexer_test_errors = Some( test_results.as_array().unwrap()[0] .get("errorMessage") .unwrap() .to_string(), ); + } else { + app.data.radarr_data.indexer_test_errors = Some(String::new()); }; }) .await @@ -2249,7 +2252,7 @@ impl<'a, 'b> Network<'a, 'b> { let tags = edit_tags.clone(); let missing_tags_vec = edit_tags .split(',') - .filter(|&tag| !tag.is_empty() && tags_map.get_by_right(tag.trim()).is_none()) + .filter(|&tag| !tag.is_empty() && tags_map.get_by_right(tag.to_lowercase().trim()).is_none()) .collect::>(); for tag in missing_tags_vec { @@ -2268,7 +2271,7 @@ impl<'a, 'b> Network<'a, 'b> { .data .radarr_data .tags_map - .get_by_right(tag.trim()) + .get_by_right(tag.to_lowercase().trim()) .unwrap() }) .collect() diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index c75ffd3..fa2b5a0 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -780,7 +780,7 @@ mod test { .is_none()); assert_eq!( app_arc.lock().await.get_current_route(), - &ActiveRadarrBlock::AddMovieEmptySearchResults.into() + ActiveRadarrBlock::AddMovieEmptySearchResults.into() ); } @@ -831,7 +831,7 @@ mod test { .is_none()); assert_eq!( app_arc.lock().await.get_current_route(), - &ActiveRadarrBlock::Movies.into() + ActiveRadarrBlock::Movies.into() ); } @@ -931,7 +931,7 @@ mod test { async_details_server.assert_async().await; async_test_server.assert_async().await; assert_eq!( - app_arc.lock().await.data.radarr_data.indexer_test_error, + app_arc.lock().await.data.radarr_data.indexer_test_errors, Some("\"test failure\"".to_owned()) ); assert_eq!(value, response_json) @@ -1000,8 +1000,8 @@ mod test { async_details_server.assert_async().await; async_test_server.assert_async().await; assert_eq!( - app_arc.lock().await.data.radarr_data.indexer_test_error, - None + app_arc.lock().await.data.radarr_data.indexer_test_errors, + Some(String::new()) ); assert_eq!(value, json!({})); } @@ -4082,7 +4082,7 @@ mod test { "enableAutomaticSearch": false, "enableInteractiveSearch": false, "name": "Test Update", - "priority": 1, + "priority": 0, "fields": [ { "name": "baseUrl", @@ -4134,6 +4134,7 @@ mod test { api_key: "test1234".into(), seed_ratio: "1.3".into(), tags: "usenet, testing".into(), + priority: 0, }; app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); app.data.radarr_data.indexers.set_items(vec![indexer()]); @@ -4179,7 +4180,7 @@ mod test { "enableAutomaticSearch": false, "enableInteractiveSearch": false, "name": "Test Update", - "priority": 1, + "priority": 0, "fields": [ { "name": "baseUrl", @@ -4227,6 +4228,7 @@ mod test { api_key: "test1234".into(), seed_ratio: "1.3".into(), tags: "usenet, testing".into(), + priority: 0, }; app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); let mut indexer = indexer(); @@ -4284,7 +4286,7 @@ mod test { "enableAutomaticSearch": false, "enableInteractiveSearch": false, "name": "Test Update", - "priority": 1, + "priority": 0, "fields": [ { "name": "baseUrl", @@ -4336,6 +4338,7 @@ mod test { api_key: "test1234".into(), seed_ratio: "1.3".into(), tags: "usenet, testing".into(), + priority: 0, }; app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); let mut indexer = indexer(); @@ -4879,7 +4882,7 @@ mod test { #[tokio::test] async fn test_extract_and_add_radarr_tag_ids_vec() { let app_arc = Arc::new(Mutex::new(App::default())); - let tags = " test,hi ,, usenet ".to_owned(); + let tags = " test,HI ,, usenet ".to_owned(); { let mut app = app_arc.lock().await; app.data.radarr_data.tags_map = BiMap::from_iter([ @@ -4900,7 +4903,7 @@ mod test { async fn test_extract_and_add_radarr_tag_ids_vec_add_missing_tags_first() { let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, - Some(json!({ "label": "testing" })), + Some(json!({ "label": "TESTING" })), Some(json!({ "id": 3, "label": "testing" })), None, RadarrEvent::GetTags, @@ -4908,7 +4911,7 @@ mod test { None, ) .await; - let tags = "usenet, test, testing".to_owned(); + let tags = "usenet, test, TESTING".to_owned(); { let mut app = app_arc.lock().await; app.data.radarr_data.edit_movie_modal = Some(EditMovieModal { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 6a0d84a..2dc10be 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -4,6 +4,8 @@ use log::{debug, info, warn}; use serde_json::{json, Value}; use urlencoding::encode; +use super::{Network, NetworkEvent, NetworkResource}; +use crate::models::sonarr_models::{DownloadStatus, MonitorEpisodeBody}; use crate::{ models::{ radarr_models::IndexerTestResult, @@ -19,11 +21,11 @@ use crate::{ LogResponse, QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update, }, sonarr_models::{ - AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, BlocklistResponse, + AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DeleteSeriesParams, DownloadRecord, DownloadsResponse, EditSeriesParams, Episode, - IndexerSettings, Series, SonarrCommandBody, SonarrHistoryItem, SonarrHistoryWrapper, - SonarrRelease, SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask, SonarrTaskName, - SystemStatus, + EpisodeFile, IndexerSettings, Series, SonarrCommandBody, SonarrHistoryItem, + SonarrHistoryWrapper, SonarrRelease, SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask, + SonarrTaskName, SystemStatus, }, stateful_table::StatefulTable, HorizontallyScrollableText, Route, Scrollable, ScrollableText, @@ -31,8 +33,6 @@ use crate::{ network::RequestMethod, utils::convert_to_gb, }; - -use super::{Network, NetworkEvent, NetworkResource}; #[cfg(test)] #[path = "sonarr_network_tests.rs"] mod sonarr_network_tests; @@ -62,6 +62,7 @@ pub enum SonarrEvent { GetIndexers, GetEpisodeDetails(Option), GetEpisodes(Option), + GetEpisodeFiles(Option), GetEpisodeHistory(Option), GetLanguageProfiles, GetLogs(Option), @@ -70,6 +71,7 @@ pub enum SonarrEvent { GetQueuedEvents, GetRootFolders, GetEpisodeReleases(Option), + GetSeasonHistory(Option<(i64, i64)>), GetSeasonReleases(Option<(i64, i64)>), GetSecurityConfig, GetSeriesDetails(Option), @@ -85,6 +87,8 @@ pub enum SonarrEvent { StartTask(Option), TestIndexer(Option), TestAllIndexers, + ToggleSeasonMonitoring(Option<(i64, i64)>), + ToggleEpisodeMonitoring(Option), TriggerAutomaticEpisodeSearch(Option), TriggerAutomaticSeasonSearch(Option<(i64, i64)>), TriggerAutomaticSeriesSearch(Option), @@ -103,7 +107,7 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetAllIndexerSettings | SonarrEvent::EditAllIndexerSettings(_) => { "/config/indexer" } - SonarrEvent::DeleteEpisodeFile(_) => "/episodefile", + SonarrEvent::GetEpisodeFiles(_) | SonarrEvent::DeleteEpisodeFile(_) => "/episodefile", SonarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000", SonarrEvent::GetDownloads | SonarrEvent::DeleteDownload(_) => "/queue", SonarrEvent::GetEpisodes(_) | SonarrEvent::GetEpisodeDetails(_) => "/episode", @@ -112,7 +116,7 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetIndexers | SonarrEvent::DeleteIndexer(_) | SonarrEvent::EditIndexer(_) => { "/indexer" } - SonarrEvent::GetLanguageProfiles => "/languageprofile", + SonarrEvent::GetLanguageProfiles => "/language", SonarrEvent::GetLogs(_) => "/log", SonarrEvent::GetDiskSpace => "/diskspace", SonarrEvent::GetQualityProfiles => "/qualityprofile", @@ -128,7 +132,7 @@ impl NetworkResource for SonarrEvent { | SonarrEvent::DeleteRootFolder(_) | SonarrEvent::AddRootFolder(_) => "/rootfolder", SonarrEvent::GetSeasonReleases(_) | SonarrEvent::GetEpisodeReleases(_) => "/release", - SonarrEvent::GetSeriesHistory(_) => "/history/series", + SonarrEvent::GetSeriesHistory(_) | SonarrEvent::GetSeasonHistory(_) => "/history/series", SonarrEvent::GetStatus => "/system/status", SonarrEvent::GetTasks => "/system/task", SonarrEvent::GetUpdates => "/update", @@ -137,11 +141,13 @@ impl NetworkResource for SonarrEvent { | SonarrEvent::ListSeries | SonarrEvent::GetSeriesDetails(_) | SonarrEvent::DeleteSeries(_) - | SonarrEvent::EditSeries(_) => "/series", + | SonarrEvent::EditSeries(_) + | SonarrEvent::ToggleSeasonMonitoring(_) => "/series", SonarrEvent::SearchNewSeries(_) => "/series/lookup", SonarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", SonarrEvent::TestIndexer(_) => "/indexer/test", SonarrEvent::TestAllIndexers => "/indexer/testall", + SonarrEvent::ToggleEpisodeMonitoring(_) => "/episode/monitor", } } } @@ -224,6 +230,10 @@ impl<'a, 'b> Network<'a, 'b> { .get_episodes(series_id) .await .map(SonarrSerdeable::from), + SonarrEvent::GetEpisodeFiles(series_id) => self + .get_episode_files(series_id) + .await + .map(SonarrSerdeable::from), SonarrEvent::GetEpisodeDetails(episode_id) => self .get_episode_details(episode_id) .await @@ -266,6 +276,10 @@ impl<'a, 'b> Network<'a, 'b> { .get_episode_releases(params) .await .map(SonarrSerdeable::from), + SonarrEvent::GetSeasonHistory(params) => self + .get_sonarr_season_history(params) + .await + .map(SonarrSerdeable::from), SonarrEvent::GetSeasonReleases(params) => self .get_season_releases(params) .await @@ -311,6 +325,14 @@ impl<'a, 'b> Network<'a, 'b> { .test_all_sonarr_indexers() .await .map(SonarrSerdeable::from), + SonarrEvent::ToggleEpisodeMonitoring(episode_id) => self + .toggle_sonarr_episode_monitoring(episode_id) + .await + .map(SonarrSerdeable::from), + SonarrEvent::ToggleSeasonMonitoring(params) => self + .toggle_sonarr_season_monitoring(params) + .await + .map(SonarrSerdeable::from), SonarrEvent::TriggerAutomaticSeasonSearch(params) => self .trigger_automatic_season_search(params) .await @@ -857,10 +879,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit indexer body"); - let mut detailed_indexer_body: Value = serde_json::from_str(&response).unwrap(); - let priority = detailed_indexer_body["priority"] - .as_i64() - .expect("Unable to deserialize 'priority'"); + let mut detailed_indexer_body: Value = serde_json::from_str(&response)?; let ( name, @@ -873,6 +892,9 @@ impl<'a, 'b> Network<'a, 'b> { tags, priority, ) = if let Some(params) = edit_indexer_params { + let priority = detailed_indexer_body["priority"] + .as_i64() + .expect("Unable to deserialize 'priority'"); let seed_ratio_field_option = detailed_indexer_body["fields"] .as_array() .unwrap() @@ -987,6 +1009,7 @@ impl<'a, 'b> Network<'a, 'b> { url, api_key, seed_ratio, + priority, .. } = app.data.sonarr_data.edit_indexer_modal.as_ref().unwrap(); @@ -999,7 +1022,7 @@ impl<'a, 'b> Network<'a, 'b> { api_key.text.clone(), seed_ratio.text.clone(), tag_ids_vec, - priority, + *priority, ) }; @@ -1104,7 +1127,7 @@ impl<'a, 'b> Network<'a, 'b> { info!("Constructing edit series body"); - let mut detailed_series_body: Value = serde_json::from_str(&response).unwrap(); + let mut detailed_series_body: Value = serde_json::from_str(&response)?; let ( monitored, use_season_folders, @@ -1257,6 +1280,91 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn toggle_sonarr_season_monitoring( + &mut self, + series_id_season_number_tuple: Option<(i64, i64)>, + ) -> Result<()> { + let detail_event = SonarrEvent::GetSeriesDetails(None); + let event = SonarrEvent::ToggleSeasonMonitoring(series_id_season_number_tuple); + let (series_id, season_number) = + if let Some((series_id, season_number)) = series_id_season_number_tuple { + (Some(series_id), Some(season_number)) + } else { + (None, None) + }; + + let (series_id, _) = self.extract_series_id(series_id).await; + if let Ok((season_number, _)) = self.extract_season_number(season_number).await { + info!("Toggling season monitoring for season {season_number} in series with ID: {series_id}"); + info!("Fetching series details for series with ID: {series_id}"); + + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{series_id}")), + None, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_series_body, _| { + response = detailed_series_body.to_string() + }) + .await?; + + info!("Constructing toggle season monitoring body"); + + let mut detailed_series_body: Value = + serde_json::from_str(&response).expect("Request for detailed series body was interrupted"); + let monitored = detailed_series_body + .get("seasons") + .unwrap() + .as_array() + .unwrap() + .iter() + .find(|season| season["seasonNumber"] == season_number) + .unwrap() + .get("monitored") + .unwrap() + .as_bool() + .unwrap(); + + *detailed_series_body + .get_mut("seasons") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|season| season["seasonNumber"] == season_number) + .unwrap() + .get_mut("monitored") + .unwrap() = json!(!monitored); + + debug!("Toggle season monitoring body: {detailed_series_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Put, + Some(detailed_series_body), + Some(format!("/{series_id}")), + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } else { + warn!("Season number was not provided. Aborting..."); + Ok(()) + } + } + async fn get_all_sonarr_indexer_settings(&mut self) -> Result { info!("Fetching Sonarr indexer settings"); let event = SonarrEvent::GetAllIndexerSettings; @@ -1303,7 +1411,27 @@ impl<'a, 'b> Network<'a, 'b> { app.get_current_route(), Route::Sonarr(ActiveSonarrBlock::BlocklistSortPrompt, _) ) { - let mut blocklist_vec = blocklist_resp.records; + let mut blocklist_vec: Vec = blocklist_resp + .records + .into_iter() + .map(|item| { + if let Some(series) = app + .data + .sonarr_data + .series + .items + .iter() + .find(|it| it.id == item.series_id) + { + BlocklistItem { + series_title: Some(series.title.text.clone()), + ..item + } + } else { + item + } + }) + .collect(); blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id)); app.data.sonarr_data.blocklist.set_items(blocklist_vec); app.data.sonarr_data.blocklist.apply_sorting_toggle(false); @@ -1357,6 +1485,22 @@ impl<'a, 'b> Network<'a, 'b> { app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); } + let season_episodes_vec = if !app.data.sonarr_data.seasons.is_empty() { + let season_number = app + .data + .sonarr_data + .seasons + .current_selection() + .season_number; + + episode_vec + .into_iter() + .filter(|episode| episode.season_number == season_number) + .collect() + } else { + episode_vec + }; + app .data .sonarr_data @@ -1364,7 +1508,7 @@ impl<'a, 'b> Network<'a, 'b> { .as_mut() .unwrap() .episodes - .set_items(episode_vec.clone()); + .set_items(season_episodes_vec); app .data .sonarr_data @@ -1378,6 +1522,39 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn get_episode_files(&mut self, series_id: Option) -> Result> { + let event = SonarrEvent::GetEpisodeFiles(series_id); + let (id, series_id_param) = self.extract_series_id(series_id).await; + info!("Fetching episodes files for Sonarr series with ID: {id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(series_id_param), + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |episode_file_vec, mut app| { + if app.data.sonarr_data.season_details_modal.is_none() { + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + } + + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_files + .set_items(episode_file_vec); + }) + .await + } + async fn get_sonarr_episode_history( &mut self, episode_id: Option, @@ -1462,6 +1639,28 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::<(), Episode>(request_props, |episode_response, mut app| { + if app.cli_mode { + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + } + + if app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("Season details modal is empty") + .episode_details_modal + .is_none() + { + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal = Some(EpisodeDetailsModal::default()); + } + let Episode { id, title, @@ -1479,20 +1678,26 @@ impl<'a, 'b> Network<'a, 'b> { } else { String::new() }; - let mut episode_details_modal = EpisodeDetailsModal { - episode_details: ScrollableText::with_string(formatdoc!( - " + let episode_details_modal = app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap(); + episode_details_modal.episode_details = ScrollableText::with_string(formatdoc!( + " Title: {} Season: {season_number} Episode Number: {episode_number} Air Date: {air_date} Status: {status} Description: {}", - title.unwrap_or_default(), - overview.unwrap_or_default(), - )), - ..EpisodeDetailsModal::default() - }; + title, + overview.unwrap_or_default(), + )); if let Some(file) = episode_file { let size = convert_to_gb(file.size); episode_details_modal.file_details = formatdoc!( @@ -1504,7 +1709,7 @@ impl<'a, 'b> Network<'a, 'b> { Date Added: {}", file.relative_path, file.path, - file.language.name, + file.languages.first().unwrap_or(&Language::default()).name, file.date_added, ); @@ -1544,16 +1749,6 @@ impl<'a, 'b> Network<'a, 'b> { ); } }; - - if !app.cli_mode { - app - .data - .sonarr_data - .season_details_modal - .as_mut() - .expect("Season details modal is empty") - .episode_details_modal = Some(episode_details_modal); - } }) .await } @@ -1813,7 +2008,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let (series_id, series_id_param) = self.extract_series_id(series_id).await; - let (season_number, season_number_param) = self.extract_season_number(season_number).await; + let (season_number, season_number_param) = self.extract_season_number(season_number).await?; info!("Fetching releases for series with ID: {series_id} and season number: {season_number}"); @@ -1850,6 +2045,56 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn get_sonarr_season_history( + &mut self, + series_season_id_tuple: Option<(i64, i64)>, + ) -> Result> { + let event = SonarrEvent::GetSeasonHistory(None); + let (series_id, season_number) = + if let Some((series_id, season_number)) = series_season_id_tuple { + (Some(series_id), Some(season_number)) + } else { + (None, None) + }; + + let (series_id, series_id_param) = self.extract_series_id(series_id).await; + let (season_number, season_number_param) = self.extract_season_number(season_number).await?; + + info!("Fetching history for series with ID: {series_id} and season number: {season_number}"); + + let params = format!("{series_id_param}&{season_number_param}",); + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) + .await; + + self + .handle_request::<(), Vec>(request_props, |history_items, mut app| { + if app.data.sonarr_data.season_details_modal.is_none() { + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + } + + let mut history_vec = history_items; + history_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .set_items(history_vec); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .apply_sorting_toggle(false); + }) + .await + } + async fn get_sonarr_security_config(&mut self) -> Result { info!("Fetching Sonarr security config"); let event = SonarrEvent::GetSecurityConfig; @@ -2238,12 +2483,14 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::(request_props, |test_results, mut app| { if test_results.as_object().is_none() { - app.data.sonarr_data.indexer_test_error = Some( + app.data.sonarr_data.indexer_test_errors = Some( test_results.as_array().unwrap()[0] .get("errorMessage") .unwrap() .to_string(), ); + } else { + app.data.sonarr_data.indexer_test_errors = Some(String::new()); }; }) .await @@ -2296,6 +2543,64 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn toggle_sonarr_episode_monitoring(&mut self, episode_id: Option) -> Result<()> { + let event = SonarrEvent::ToggleEpisodeMonitoring(episode_id); + let detail_event = SonarrEvent::GetEpisodeDetails(None); + + let (id, monitored) = if let Some(episode_id) = episode_id { + info!("Fetching episode details for episode id: {episode_id}"); + let request_props = self + .request_props_from( + detail_event, + RequestMethod::Get, + None::<()>, + Some(format!("/{episode_id}")), + None, + ) + .await; + + let mut monitored = false; + + self + .handle_request::<(), Value>(request_props, |detailed_episode_body, _| { + monitored = detailed_episode_body + .get("monitored") + .unwrap() + .as_bool() + .unwrap(); + }) + .await?; + + (episode_id, monitored) + } else { + let app = self.app.lock().await; + let current_selection = app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episodes + .current_selection(); + (current_selection.id, current_selection.monitored) + }; + + info!("Toggling monitoring for episode id: {id}"); + + let body = MonitorEpisodeBody { + episode_ids: vec![id], + monitored: !monitored, + }; + + let request_props = self + .request_props_from(event, RequestMethod::Put, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + async fn trigger_automatic_series_search(&mut self, series_id: Option) -> Result { let event = SonarrEvent::TriggerAutomaticSeriesSearch(series_id); let (id, _) = self.extract_series_id(series_id).await; @@ -2329,7 +2634,7 @@ impl<'a, 'b> Network<'a, 'b> { }; let (series_id, _) = self.extract_series_id(series_id).await; - let (season_number, _) = self.extract_season_number(season_number).await; + let (season_number, _) = self.extract_season_number(season_number).await?; info!("Searching indexers for series with ID: {series_id} and season number: {season_number}"); let body = SonarrCommandBody { @@ -2425,7 +2730,7 @@ impl<'a, 'b> Network<'a, 'b> { let tags = edit_tags.clone(); let missing_tags_vec = edit_tags .split(',') - .filter(|&tag| !tag.is_empty() && tags_map.get_by_right(tag.trim()).is_none()) + .filter(|&tag| !tag.is_empty() && tags_map.get_by_right(tag.to_lowercase().trim()).is_none()) .collect::>(); for tag in missing_tags_vec { @@ -2444,7 +2749,7 @@ impl<'a, 'b> Network<'a, 'b> { .data .sonarr_data .tags_map - .get_by_right(tag.trim()) + .get_by_right(tag.to_lowercase().trim()) .unwrap() }) .collect() @@ -2467,11 +2772,11 @@ impl<'a, 'b> Network<'a, 'b> { (series_id, format!("seriesId={series_id}")) } - async fn extract_season_number(&mut self, season_number: Option) -> (i64, String) { - let season_number = if let Some(number) = season_number { - number - } else { - self + async fn extract_season_number(&mut self, season_number: Option) -> Result<(i64, String)> { + if let Some(number) = season_number { + Ok((number, format!("seasonNumber={number}"))) + } else if !self.app.lock().await.data.sonarr_data.seasons.is_empty() { + let season_number = self .app .lock() .await @@ -2479,9 +2784,11 @@ impl<'a, 'b> Network<'a, 'b> { .sonarr_data .seasons .current_selection() - .season_number - }; - (season_number, format!("seasonNumber={season_number}")) + .season_number; + Ok((season_number, format!("seasonNumber={season_number}"))) + } else { + Err(anyhow!("No season number provided")) + } } async fn extract_episode_id(&mut self, episode_id: Option) -> i64 { @@ -2512,11 +2819,11 @@ fn get_episode_status(has_file: bool, downloads_vec: &[DownloadRecord], episode_ .iter() .find(|&download| download.episode_id == episode_id) { - if download.status == "downloading" { + if download.status == DownloadStatus::Downloading { return "Downloading".to_owned(); } - if download.status == "completed" { + if download.status == DownloadStatus::Completed { return "Awaiting Import".to_owned(); } } diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 3b5e058..ee9425d 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -3,7 +3,7 @@ mod test { use std::sync::Arc; use bimap::BiMap; - use chrono::{DateTime, Utc}; + use chrono::DateTime; use indoc::formatdoc; use mockito::{Matcher, Server}; use pretty_assertions::{assert_eq, assert_str_eq}; @@ -17,7 +17,8 @@ mod test { use crate::models::sonarr_models::{ AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, AddSeriesSearchResultStatistics, - EditSeriesParams, IndexerSettings, SeriesMonitor, + DownloadStatus, EditSeriesParams, IndexerSettings, MonitorEpisodeBody, SeriesMonitor, + SonarrHistoryEventType, }; use crate::app::{App, ServarrConfig}; @@ -108,11 +109,12 @@ mod test { "airDateUtc": "2024-02-10T07:28:45Z", "overview": "Okay so this one time at band camp...", "episodeFile": { + "id": 1, "relativePath": "/season 1/episode 1.mkv", "path": "/nfs/tv/series/season 1/episode 1.mkv", "size": 3543348019, "dateAdded": "2024-02-10T07:28:45Z", - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "quality": { "quality": { "name": "Bluray-1080p" } }, "mediaInfo": { "audioBitrate": 0, @@ -161,7 +163,8 @@ mod test { SonarrEvent::ListSeries, SonarrEvent::GetSeriesDetails(None), SonarrEvent::DeleteSeries(None), - SonarrEvent::EditSeries(None) + SonarrEvent::EditSeries(None), + SonarrEvent::ToggleSeasonMonitoring(None) )] event: SonarrEvent, ) { @@ -224,6 +227,17 @@ mod test { assert_str_eq!(event.resource(), "/history"); } + #[rstest] + fn test_resource_series_history( + #[values( + SonarrEvent::GetSeriesHistory(None), + SonarrEvent::GetSeasonHistory(None) + )] + event: SonarrEvent, + ) { + assert_str_eq!(event.resource(), "/history/series"); + } + #[rstest] fn test_resource_queue( #[values(SonarrEvent::GetDownloads, SonarrEvent::DeleteDownload(None))] event: SonarrEvent, @@ -254,15 +268,24 @@ mod test { assert_str_eq!(event.resource(), "/release"); } + #[rstest] + fn test_resource_episode_file( + #[values( + SonarrEvent::GetEpisodeFiles(None), + SonarrEvent::DeleteEpisodeFile(None) + )] + event: SonarrEvent, + ) { + assert_str_eq!(event.resource(), "/episodefile"); + } + #[rstest] #[case(SonarrEvent::ClearBlocklist, "/blocklist/bulk")] #[case(SonarrEvent::DeleteBlocklistItem(None), "/blocklist")] - #[case(SonarrEvent::DeleteEpisodeFile(None), "/episodefile")] #[case(SonarrEvent::HealthCheck, "/health")] #[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] #[case(SonarrEvent::GetDiskSpace, "/diskspace")] - #[case(SonarrEvent::GetSeriesHistory(None), "/history/series")] - #[case(SonarrEvent::GetLanguageProfiles, "/languageprofile")] + #[case(SonarrEvent::GetLanguageProfiles, "/language")] #[case(SonarrEvent::GetLogs(Some(500)), "/log")] #[case(SonarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(SonarrEvent::GetStatus, "/system/status")] @@ -272,6 +295,7 @@ mod test { #[case(SonarrEvent::SearchNewSeries(None), "/series/lookup")] #[case(SonarrEvent::TestIndexer(None), "/indexer/test")] #[case(SonarrEvent::TestAllIndexers, "/indexer/testall")] + #[case(SonarrEvent::ToggleEpisodeMonitoring(None), "/episode/monitor")] fn test_resource(#[case] event: SonarrEvent, #[case] expected_uri: String) { assert_str_eq!(event.resource(), expected_uri); } @@ -1162,7 +1186,7 @@ mod test { "enableAutomaticSearch": false, "enableInteractiveSearch": false, "name": "Test Update", - "priority": 1, + "priority": 0, "fields": [ { "name": "baseUrl", @@ -1218,6 +1242,7 @@ mod test { api_key: "test1234".into(), seed_ratio: "1.3".into(), tags: "usenet, testing".into(), + priority: 0, }; app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal); app.data.sonarr_data.indexers.set_items(vec![indexer()]); @@ -1263,7 +1288,7 @@ mod test { "enableAutomaticSearch": false, "enableInteractiveSearch": false, "name": "Test Update", - "priority": 1, + "priority": 0, "fields": [ { "name": "baseUrl", @@ -1315,6 +1340,7 @@ mod test { api_key: "test1234".into(), seed_ratio: "1.3".into(), tags: "usenet, testing".into(), + priority: 0, }; app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal); let mut indexer = indexer(); @@ -1372,7 +1398,7 @@ mod test { "enableAutomaticSearch": false, "enableInteractiveSearch": false, "name": "Test Update", - "priority": 1, + "priority": 0, "fields": [ { "name": "baseUrl", @@ -1428,6 +1454,7 @@ mod test { api_key: "test1234".into(), seed_ratio: "1.3".into(), tags: "usenet, testing".into(), + priority: 0, }; app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal); let mut indexer = indexer(); @@ -1931,7 +1958,7 @@ mod test { "seriesId": 1007, "episodeIds": [42020], "sourceTitle": "z series", - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "quality": { "quality": { "name": "Bluray-1080p" }}, "date": "2024-02-10T07:28:45Z", "protocol": "usenet", @@ -1943,7 +1970,7 @@ mod test { "seriesId": 2001, "episodeIds": [42018], "sourceTitle": "A Series", - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "quality": { "quality": { "name": "Bluray-1080p" }}, "date": "2024-02-10T07:28:45Z", "protocol": "usenet", @@ -1956,6 +1983,7 @@ mod test { BlocklistItem { id: 123, series_id: 1007, + series_title: Some("Z Series".into()), source_title: "z series".into(), episode_ids: vec![Number::from(42020)], ..blocklist_item() @@ -1978,6 +2006,17 @@ mod test { None, ) .await; + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![Series { + id: 1007, + title: "Z Series".into(), + ..series() + }]); app_arc.lock().await.data.sonarr_data.blocklist.sort_asc = true; if use_custom_sorting { let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| { @@ -2022,7 +2061,7 @@ mod test { "seriesId": 1007, "episodeIds": [42020], "sourceTitle": "z series", - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "quality": { "quality": { "name": "Bluray-1080p" }}, "date": "2024-02-10T07:28:45Z", "protocol": "usenet", @@ -2034,7 +2073,7 @@ mod test { "seriesId": 2001, "episodeIds": [42018], "sourceTitle": "A Series", - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "quality": { "quality": { "name": "Bluray-1080p" }}, "date": "2024-02-10T07:28:45Z", "protocol": "usenet", @@ -2201,25 +2240,34 @@ mod test { #[tokio::test] async fn test_handle_get_episodes_event(#[values(true, false)] use_custom_sorting: bool) { let episode_1 = Episode { - title: Some("z test".to_owned()), + title: "z test".to_owned(), episode_file: None, ..episode() }; let episode_2 = Episode { id: 2, - title: Some("A test".to_owned()), + title: "A test".to_owned(), episode_file_id: 2, season_number: 2, episode_number: 2, episode_file: None, ..episode() }; - let expected_episodes = vec![episode_1.clone(), episode_2.clone()]; - let mut expected_sorted_episodes = vec![episode_1.clone(), episode_2.clone()]; + let episode_3 = Episode { + id: 3, + title: "A test".to_owned(), + episode_file_id: 3, + season_number: 1, + episode_number: 2, + episode_file: None, + ..episode() + }; + let expected_episodes = vec![episode_1.clone(), episode_2.clone(), episode_3.clone()]; + let mut expected_sorted_episodes = vec![episode_1.clone(), episode_3.clone()]; let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, None, - Some(json!([episode_1, episode_2])), + Some(json!([episode_1, episode_2, episode_3])), None, SonarrEvent::GetEpisodes(None), None, @@ -2229,13 +2277,7 @@ mod test { let mut season_details_modal = SeasonDetailsModal::default(); season_details_modal.episodes.sort_asc = true; if use_custom_sorting { - let cmp_fn = |a: &Episode, b: &Episode| { - a.title - .as_ref() - .unwrap() - .to_lowercase() - .cmp(&b.title.as_ref().unwrap().to_lowercase()) - }; + let cmp_fn = |a: &Episode, b: &Episode| a.title.to_lowercase().cmp(&b.title.to_lowercase()); expected_sorted_episodes.sort_by(cmp_fn); let title_sort_option = SortOption { name: "Title", @@ -2246,6 +2288,102 @@ mod test { .sorting(vec![title_sort_option]); } app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![Series { + id: 1, + ..Series::default() + }]); + app_arc + .lock() + .await + .data + .sonarr_data + .seasons + .set_items(vec![Season { + season_number: 1, + ..Season::default() + }]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::Episodes(episodes) = network + .handle_sonarr_event(SonarrEvent::GetEpisodes(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episodes + .items, + expected_sorted_episodes + ); + assert!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episodes + .sort_asc + ); + assert_eq!(episodes, expected_episodes); + } + } + + #[tokio::test] + async fn test_handle_get_episodes_event_empty_seasons_table_returns_all_episodes_by_default() { + let episode_1 = Episode { + title: "z test".to_owned(), + episode_file: None, + ..episode() + }; + let episode_2 = Episode { + id: 2, + title: "A test".to_owned(), + episode_file_id: 2, + season_number: 2, + episode_number: 2, + episode_file: None, + ..episode() + }; + let episode_3 = Episode { + id: 3, + title: "A test".to_owned(), + episode_file_id: 3, + season_number: 1, + episode_number: 2, + episode_file: None, + ..episode() + }; + let expected_episodes = vec![episode_1.clone(), episode_2.clone(), episode_3.clone()]; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([episode_1, episode_2, episode_3])), + None, + SonarrEvent::GetEpisodes(None), + None, + Some("seriesId=1"), + ) + .await; + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.sort_asc = true; + app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); app_arc .lock() .await @@ -2275,7 +2413,7 @@ mod test { .unwrap() .episodes .items, - expected_sorted_episodes + expected_episodes ); assert!( app_arc @@ -2399,13 +2537,7 @@ mod test { .push_navigation_stack(ActiveSonarrBlock::EpisodesSortPrompt.into()); let mut season_details_modal = SeasonDetailsModal::default(); season_details_modal.episodes.sort_asc = true; - let cmp_fn = |a: &Episode, b: &Episode| { - a.title - .as_ref() - .unwrap() - .to_lowercase() - .cmp(&b.title.as_ref().unwrap().to_lowercase()) - }; + let cmp_fn = |a: &Episode, b: &Episode| a.title.to_lowercase().cmp(&b.title.to_lowercase()); expected_episodes.sort_by(cmp_fn); let title_sort_option = SortOption { name: "Title", @@ -2459,6 +2591,162 @@ mod test { } } + #[tokio::test] + async fn test_handle_get_episode_files_event() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([episode_file()])), + None, + SonarrEvent::GetEpisodeFiles(None), + None, + Some("seriesId=1"), + ) + .await; + app_arc.lock().await.data.sonarr_data.season_details_modal = + Some(SeasonDetailsModal::default()); + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![Series { + id: 1, + ..Series::default() + }]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::EpisodeFiles(episode_files) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_files + .items, + vec![episode_file()] + ); + assert_eq!(episode_files, vec![episode_file()]); + } + } + + #[tokio::test] + async fn test_handle_get_episode_files_event_empty_season_details_modal() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([episode_file()])), + None, + SonarrEvent::GetEpisodeFiles(None), + None, + Some("seriesId=1"), + ) + .await; + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![Series { + id: 1, + ..Series::default() + }]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::EpisodeFiles(episode_files) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .is_some()); + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_files + .items, + vec![episode_file()] + ); + assert_eq!(episode_files, vec![episode_file()]); + } + } + + #[tokio::test] + async fn test_handle_get_episode_files_event_uses_provided_series_id() { + let episode_file = EpisodeFile { + id: 2, + ..episode_file() + }; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([episode_file.clone()])), + None, + SonarrEvent::GetEpisodeFiles(Some(2)), + None, + Some("seriesId=2"), + ) + .await; + app_arc.lock().await.data.sonarr_data.season_details_modal = + Some(SeasonDetailsModal::default()); + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![Series { + id: 1, + ..Series::default() + }]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::EpisodeFiles(episode_files) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(Some(2))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_files + .items, + vec![episode_file.clone()] + ); + assert_eq!(episode_files, vec![episode_file]); + } + } + #[tokio::test] async fn test_handle_get_sonarr_host_config_event() { let host_config_response = json!({ @@ -2503,7 +2791,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -2516,7 +2804,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -2595,7 +2883,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -2608,7 +2896,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -2666,7 +2954,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -2679,7 +2967,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -2862,6 +3150,122 @@ mod test { #[tokio::test] async fn test_handle_get_episode_details_event() { + let response: Episode = serde_json::from_str(EPISODE_JSON).unwrap(); + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(EPISODE_JSON).unwrap()), + None, + SonarrEvent::GetEpisodeDetails(None), + Some("/1"), + None, + ) + .await; + let mut episode_details_modal = EpisodeDetailsModal::default(); + episode_details_modal.episode_details_tabs.next(); + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + season_details_modal.episode_details_modal = Some(episode_details_modal); + app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); + app_arc + .lock() + .await + .push_navigation_stack(ActiveSonarrBlock::EpisodeDetails.into()); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::Episode(episode) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeDetails(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .is_some()); + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_details_tabs + .get_active_route(), + ActiveSonarrBlock::EpisodeHistory.into() + ); + assert_eq!(episode, response); + + let app = app_arc.lock().await; + let episode_details_modal = app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap(); + assert_str_eq!( + episode_details_modal.episode_details.get_text(), + formatdoc!( + "Title: Something cool + Season: 1 + Episode Number: 1 + Air Date: 2024-02-10 07:28:45 UTC + Status: Downloaded + Description: Okay so this one time at band camp..." + ) + ); + assert_str_eq!( + episode_details_modal.file_details, + formatdoc!( + "Relative Path: /season 1/episode 1.mkv + Absolute Path: /nfs/tv/series/season 1/episode 1.mkv + Size: 3.30 GB + Language: English + Date Added: 2024-02-10 07:28:45 UTC" + ) + ); + assert_str_eq!( + episode_details_modal.audio_details, + formatdoc!( + "Bitrate: 0 + Channels: 7.1 + Codec: AAC + Languages: eng + Stream Count: 1" + ) + ); + assert_str_eq!( + episode_details_modal.video_details, + formatdoc!( + "Bit Depth: 10 + Bitrate: 0 + Codec: x265 + FPS: 23.976 + Resolution: 1920x1080 + Scan Type: Progressive + Runtime: 23:51 + Subtitles: English" + ) + ); + } + } + + #[tokio::test] + async fn test_handle_get_episode_details_event_empty_episode_details_modal() { let response: Episode = serde_json::from_str(EPISODE_JSON).unwrap(); let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Get, @@ -2992,7 +3396,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -3005,7 +3409,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -3122,7 +3526,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -3135,7 +3539,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -3252,7 +3656,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -3265,7 +3669,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -3360,7 +3764,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -3373,7 +3777,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -4036,6 +4440,349 @@ mod test { } } + #[tokio::test] + async fn test_handle_get_sonarr_season_history_event() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z episode", + "episodeId": 1007, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }, + { + "id": 456, + "sourceTitle": "A Episode", + "episodeId": 2001, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![ + SonarrHistoryItem { + id: 123, + episode_id: 1007, + source_title: "z episode".into(), + ..history_item() + }, + SonarrHistoryItem { + id: 456, + episode_id: 2001, + source_title: "A Episode".into(), + ..history_item() + }, + ]; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(history_json), + None, + SonarrEvent::GetSeasonHistory(None), + None, + Some("seriesId=1&seasonNumber=1"), + ) + .await; + app_arc.lock().await.data.sonarr_data.season_details_modal = + Some(SeasonDetailsModal::default()); + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![series()]); + app_arc + .lock() + .await + .data + .sonarr_data + .seasons + .set_items(vec![season()]); + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .sort_asc = true; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::SonarrHistoryItems(history) = network + .handle_sonarr_event(SonarrEvent::GetSeasonHistory(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .items, + expected_history_items + ); + assert!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .sort_asc + ); + assert_eq!(history, response); + } + } + + #[tokio::test] + async fn test_handle_get_sonarr_season_history_event_uses_provided_series_id_and_season_number() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z episode", + "episodeId": 1007, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }, + { + "id": 456, + "sourceTitle": "A Episode", + "episodeId": 2001, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![ + SonarrHistoryItem { + id: 123, + episode_id: 1007, + source_title: "z episode".into(), + ..history_item() + }, + SonarrHistoryItem { + id: 456, + episode_id: 2001, + source_title: "A Episode".into(), + ..history_item() + }, + ]; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(history_json), + None, + SonarrEvent::GetSeasonHistory(Some((2, 2))), + None, + Some("seriesId=2&seasonNumber=2"), + ) + .await; + app_arc.lock().await.data.sonarr_data.season_details_modal = + Some(SeasonDetailsModal::default()); + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![series()]); + app_arc + .lock() + .await + .data + .sonarr_data + .seasons + .set_items(vec![season()]); + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .sort_asc = true; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::SonarrHistoryItems(history) = network + .handle_sonarr_event(SonarrEvent::GetSeasonHistory(Some((2, 2)))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .items, + expected_history_items + ); + assert!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .sort_asc + ); + assert_eq!(history, response); + } + } + + #[tokio::test] + async fn test_handle_get_sonarr_season_history_event_empty_season_details_modal() { + let history_json = json!([{ + "id": 123, + "sourceTitle": "z episode", + "episodeId": 1007, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }, + { + "id": 456, + "sourceTitle": "A Episode", + "episodeId": 2001, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "languages": [{ "id": 1, "name": "English" }], + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }]); + let response: Vec = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![ + SonarrHistoryItem { + id: 123, + episode_id: 1007, + source_title: "z episode".into(), + ..history_item() + }, + SonarrHistoryItem { + id: 456, + episode_id: 2001, + source_title: "A Episode".into(), + ..history_item() + }, + ]; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(history_json), + None, + SonarrEvent::GetSeasonHistory(None), + None, + Some("seriesId=1&seasonNumber=1"), + ) + .await; + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![series()]); + app_arc + .lock() + .await + .data + .sonarr_data + .seasons + .set_items(vec![season()]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::SonarrHistoryItems(history) = network + .handle_sonarr_event(SonarrEvent::GetSeasonHistory(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .is_some()); + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .items, + expected_history_items + ); + assert!( + !app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .sort_asc + ); + assert_eq!(history, response); + } + } + #[tokio::test] async fn test_handle_get_season_releases_event() { let release_json = json!([ @@ -4382,12 +5129,14 @@ mod test { ) .await; let mut filtered_series = StatefulTable::default(); + filtered_series.set_items(vec![Series::default()]); filtered_series.set_filtered_items(vec![Series { id: 1, ..Series::default() }]); app_arc.lock().await.data.sonarr_data.series = filtered_series; let mut filtered_seasons = StatefulTable::default(); + filtered_seasons.set_items(vec![Season::default()]); filtered_seasons.set_filtered_items(vec![Season { season_number: 1, ..Season::default() @@ -4580,7 +5329,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -4593,7 +5342,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -4702,7 +5451,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -4715,7 +5464,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -4808,7 +5557,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -4821,7 +5570,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -4910,7 +5659,7 @@ mod test { "sourceTitle": "z episode", "episodeId": 1007, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -4923,7 +5672,7 @@ mod test { "sourceTitle": "A Episode", "episodeId": 2001, "quality": { "quality": { "name": "Bluray-1080p" } }, - "language": { "id": 1, "name": "English" }, + "languages": [{ "id": 1, "name": "English" }], "date": "2024-02-10T07:28:45Z", "eventType": "grabbed", "data": { @@ -5117,8 +5866,7 @@ mod test { ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()) - as DateTime; + let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()); if let SonarrSerdeable::SystemStatus(status) = network .handle_sonarr_event(SonarrEvent::GetStatus) @@ -5474,7 +6222,7 @@ mod test { .is_none()); assert_eq!( app_arc.lock().await.get_current_route(), - &ActiveSonarrBlock::AddSeriesEmptySearchResults.into() + ActiveSonarrBlock::AddSeriesEmptySearchResults.into() ); } @@ -5529,7 +6277,7 @@ mod test { .is_none()); assert_eq!( app_arc.lock().await.get_current_route(), - &ActiveSonarrBlock::Series.into() + ActiveSonarrBlock::Series.into() ); } @@ -5666,7 +6414,7 @@ mod test { async_details_server.assert_async().await; async_test_server.assert_async().await; assert_eq!( - app_arc.lock().await.data.sonarr_data.indexer_test_error, + app_arc.lock().await.data.sonarr_data.indexer_test_errors, Some("\"test failure\"".to_owned()) ); assert_eq!(value, response_json) @@ -5735,8 +6483,8 @@ mod test { async_details_server.assert_async().await; async_test_server.assert_async().await; assert_eq!( - app_arc.lock().await.data.sonarr_data.indexer_test_error, - None + app_arc.lock().await.data.sonarr_data.indexer_test_errors, + Some(String::new()) ); assert_eq!(value, json!({})); } @@ -5897,6 +6645,207 @@ mod test { } } + #[tokio::test] + async fn test_handle_toggle_episode_monitoring_event() { + let expected_body = MonitorEpisodeBody { + episode_ids: vec![1], + monitored: false, + }; + + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Put, + Some(json!(expected_body)), + None, + None, + SonarrEvent::ToggleEpisodeMonitoring(None), + None, + None, + ) + .await; + { + let mut app = app_arc.lock().await; + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::ToggleEpisodeMonitoring(None)) + .await + .is_ok()); + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_toggle_episode_monitoring_event_uses_provided_episode_id() { + let expected_body = MonitorEpisodeBody { + episode_ids: vec![2], + monitored: false, + }; + let body = Episode { id: 2, ..episode() }; + + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!(body)), + None, + SonarrEvent::GetEpisodeDetails(None), + Some("/2"), + None, + ) + .await; + let async_toggle_server = server + .mock( + "PUT", + format!( + "/api/v3{}", + SonarrEvent::ToggleEpisodeMonitoring(None).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(json!(expected_body))) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + app.data.sonarr_data.season_details_modal = Some(season_details_modal); + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::ToggleEpisodeMonitoring(Some(2))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_toggle_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_toggle_season_monitoring_event() { + let mut expected_body: Value = serde_json::from_str(SERIES_JSON).unwrap(); + *expected_body + .get_mut("seasons") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|season| season["seasonNumber"] == 1) + .unwrap() + .get_mut("monitored") + .unwrap() = json!(false); + + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(SERIES_JSON).unwrap()), + None, + SonarrEvent::GetSeriesDetails(None), + Some("/1"), + None, + ) + .await; + let async_toggle_server = server + .mock( + "PUT", + format!( + "/api/v3{}/1", + SonarrEvent::ToggleSeasonMonitoring(None).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + app.data.sonarr_data.series.set_items(vec![series()]); + app.data.sonarr_data.seasons.set_items(vec![season()]); + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring(None)) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_toggle_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_toggle_season_monitoring_event_uses_provided_series_id_and_season_number() { + let mut detailed_response: Value = serde_json::from_str(SERIES_JSON).unwrap(); + *detailed_response + .get_mut("seasons") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|season| season["seasonNumber"] == 1) + .unwrap() + .get_mut("seasonNumber") + .unwrap() = json!(2); + let mut expected_body: Value = detailed_response.clone(); + *expected_body + .get_mut("seasons") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|season| season["seasonNumber"] == 2) + .unwrap() + .get_mut("monitored") + .unwrap() = json!(false); + + let (async_details_server, app_arc, mut server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(detailed_response), + None, + SonarrEvent::GetSeriesDetails(None), + Some("/2"), + None, + ) + .await; + let async_toggle_server = server + .mock( + "PUT", + format!( + "/api/v3{}/2", + SonarrEvent::ToggleSeasonMonitoring(Some((2, 2))).resource() + ) + .as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + app.data.sonarr_data.series.set_items(vec![series()]); + app.data.sonarr_data.seasons.set_items(vec![season()]); + } + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + assert!(network + .handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring(Some((2, 2)))) + .await + .is_ok()); + + async_details_server.assert_async().await; + async_toggle_server.assert_async().await; + } + #[tokio::test] async fn test_handle_trigger_automatic_episode_search_event() { let (async_server, app_arc, _server) = mock_servarr_api( @@ -6077,12 +7026,14 @@ mod test { ) .await; let mut filtered_series = StatefulTable::default(); + filtered_series.set_items(vec![Series::default()]); filtered_series.set_filtered_items(vec![Series { id: 1, ..Series::default() }]); app_arc.lock().await.data.sonarr_data.series = filtered_series; let mut filtered_seasons = StatefulTable::default(); + filtered_seasons.set_items(vec![Season::default()]); filtered_seasons.set_filtered_items(vec![Season { season_number: 1, ..Season::default() @@ -6265,7 +7216,7 @@ mod test { #[tokio::test] async fn test_extract_and_add_sonarr_tag_ids_vec() { let app_arc = Arc::new(Mutex::new(App::default())); - let tags = " test,hi ,, usenet ".to_owned(); + let tags = " test,HI ,, usenet ".to_owned(); { let mut app = app_arc.lock().await; app.data.sonarr_data.tags_map = BiMap::from_iter([ @@ -6286,7 +7237,7 @@ mod test { async fn test_extract_and_add_sonarr_tag_ids_vec_add_missing_tags_first() { let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, - Some(json!({ "label": "testing" })), + Some(json!({ "label": "TESTING" })), Some(json!({ "id": 3, "label": "testing" })), None, SonarrEvent::GetTags, @@ -6294,7 +7245,7 @@ mod test { None, ) .await; - let tags = "usenet, test, testing".to_owned(); + let tags = "usenet, test, TESTING".to_owned(); { let mut app = app_arc.lock().await; app.data.sonarr_data.add_series_modal = Some(AddSeriesModal { @@ -6394,7 +7345,7 @@ mod test { }]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let (id, season_number_param) = network.extract_season_number(None).await; + let (id, season_number_param) = network.extract_season_number(None).await.unwrap(); assert_eq!(id, 1); assert_str_eq!(season_number_param, "seasonNumber=1"); @@ -6414,7 +7365,7 @@ mod test { ..Season::default() }]); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let (id, season_number_param) = network.extract_season_number(Some(2)).await; + let (id, season_number_param) = network.extract_season_number(Some(2)).await.unwrap(); assert_eq!(id, 2); assert_str_eq!(season_number_param, "seasonNumber=2"); @@ -6424,6 +7375,7 @@ mod test { async fn test_extract_season_number_filtered_seasons() { let app_arc = Arc::new(Mutex::new(App::default())); let mut filtered_seasons = StatefulTable::default(); + filtered_seasons.set_items(vec![Season::default()]); filtered_seasons.set_filtered_items(vec![Season { season_number: 1, ..Season::default() @@ -6431,12 +7383,21 @@ mod test { app_arc.lock().await.data.sonarr_data.seasons = filtered_seasons; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let (id, season_number_param) = network.extract_season_number(None).await; + let (id, season_number_param) = network.extract_season_number(None).await.unwrap(); assert_eq!(id, 1); assert_str_eq!(season_number_param, "seasonNumber=1"); } + #[tokio::test] + async fn test_extract_season_number_empty_seasons_table() { + let app_arc = Arc::new(Mutex::new(App::default())); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + let season_number = network.extract_season_number(None).await; + + assert!(season_number.is_err()); + } + #[tokio::test] async fn test_extract_episode_id() { let app_arc = Arc::new(Mutex::new(App::default())); @@ -6528,7 +7489,7 @@ mod test { false, &[DownloadRecord { episode_id: 1, - status: "downloading".to_owned(), + status: DownloadStatus::Downloading, ..DownloadRecord::default() }], 1 @@ -6544,7 +7505,7 @@ mod test { false, &[DownloadRecord { episode_id: 1, - status: "completed".to_owned(), + status: DownloadStatus::Completed, ..DownloadRecord::default() }], 1 @@ -6577,9 +7538,10 @@ mod test { BlocklistItem { id: 1, series_id: 1, + series_title: None, episode_ids: vec![Number::from(1)], source_title: "Test Source Title".to_owned(), - language: language(), + languages: vec![language()], quality: quality_wrapper(), date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), protocol: "usenet".to_owned(), @@ -6591,7 +7553,7 @@ mod test { fn download_record() -> DownloadRecord { DownloadRecord { title: "Test Download Title".to_owned(), - status: "downloading".to_owned(), + status: DownloadStatus::Downloading, id: 1, episode_id: 1, size: 3543348019f64, @@ -6600,7 +7562,7 @@ mod test { "/nfs/tv/Test show/season 1/", )), indexer: "kickass torrents".to_owned(), - download_client: "transmission".to_owned(), + download_client: Some("transmission".to_owned()), } } @@ -6618,7 +7580,7 @@ mod test { episode_file_id: 1, season_number: 1, episode_number: 1, - title: Some("Something cool".to_owned()), + title: "Something cool".to_owned(), air_date_utc: Some(DateTime::from( DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap(), )), @@ -6631,10 +7593,12 @@ mod test { fn episode_file() -> EpisodeFile { EpisodeFile { + id: 1, relative_path: "/season 1/episode 1.mkv".to_owned(), path: "/nfs/tv/series/season 1/episode 1.mkv".to_owned(), size: 3543348019, - language: language(), + quality: quality_wrapper(), + languages: vec![language()], date_added: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), media_info: Some(media_info()), } @@ -6660,9 +7624,9 @@ mod test { source_title: "Test source".into(), episode_id: 1, quality: quality_wrapper(), - language: language(), + languages: vec![language()], date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), - event_type: "grabbed".into(), + event_type: SonarrHistoryEventType::Grabbed, data: history_data(), } } @@ -6753,6 +7717,7 @@ mod test { fn season() -> Season { Season { + title: None, season_number: 1, monitored: true, statistics: season_statistics(), diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5aa982b..0459c86 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,6 +8,8 @@ use ratatui::widgets::Paragraph; use ratatui::widgets::Tabs; use ratatui::widgets::Wrap; use ratatui::Frame; +use sonarr_ui::SonarrUi; +use utils::layout_block; use crate::app::App; use crate::models::{HorizontallyScrollableText, Route, TabState}; @@ -20,6 +22,7 @@ use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::popup::Size; mod radarr_ui; +mod sonarr_ui; mod styles; mod utils; mod widgets; @@ -57,9 +60,16 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) { draw_header_row(f, app, header_area); - if RadarrUi::accepts(*app.get_current_route()) { - RadarrUi::draw_context_row(f, app, context_area); - RadarrUi::draw(f, app, table_area); + match app.get_current_route() { + route if RadarrUi::accepts(route) => { + RadarrUi::draw_context_row(f, app, context_area); + RadarrUi::draw(f, app, table_area); + } + route if SonarrUi::accepts(route) => { + SonarrUi::draw_context_row(f, app, context_area); + SonarrUi::draw(f, app, table_area); + } + _ => (), } } @@ -118,41 +128,12 @@ pub fn draw_popup( popup_fn(f, app, popup_area); } -fn draw_popup_ui(f: &mut Frame<'_>, app: &mut App<'_>, size: Size) { - let (percent_x, percent_y) = size.to_percent(); - let popup_area = centered_rect(percent_x, percent_y, f.area()); - f.render_widget(Clear, popup_area); - f.render_widget(background_block(), popup_area); - T::draw(f, app, popup_area); -} - -pub fn draw_popup_over( - f: &mut Frame<'_>, - app: &mut App<'_>, - area: Rect, - background_fn: impl Fn(&mut Frame<'_>, &mut App<'_>, Rect), - popup_fn: impl Fn(&mut Frame<'_>, &mut App<'_>, Rect), - size: Size, -) { - background_fn(f, app, area); - - draw_popup(f, app, popup_fn, size); -} - -pub fn draw_popup_over_ui( - f: &mut Frame<'_>, - app: &mut App<'_>, - area: Rect, - background_fn: impl Fn(&mut Frame<'_>, &mut App<'_>, Rect), - size: Size, -) { - background_fn(f, app, area); - - draw_popup_ui::(f, app, size); -} - fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect { - f.render_widget(title_block(title), area); + if title.is_empty() { + f.render_widget(layout_block(), area); + } else { + f.render_widget(title_block(title), area); + } let [header_area, content_area] = Layout::vertical([Constraint::Length(1), Constraint::Fill(0)]) .margin(1) diff --git a/src/ui/radarr_ui/blocklist/mod.rs b/src/ui/radarr_ui/blocklist/mod.rs index 733f02c..b6b2b57 100644 --- a/src/ui/radarr_ui/blocklist/mod.rs +++ b/src/ui/radarr_ui/blocklist/mod.rs @@ -31,13 +31,11 @@ impl DrawUi for BlocklistUi { } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { + draw_blocklist_table(f, app, area); + match active_radarr_block { - ActiveRadarrBlock::Blocklist | ActiveRadarrBlock::BlocklistSortPrompt => { - draw_blocklist_table(f, app, area) - } ActiveRadarrBlock::BlocklistItemDetails => { - draw_blocklist_table(f, app, area); draw_blocklist_item_details_popup(f, app); } ActiveRadarrBlock::DeleteBlocklistItemPrompt => { @@ -55,8 +53,10 @@ impl DrawUi for BlocklistUi { .prompt(&prompt) .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_blocklist_table(f, app, area); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } ActiveRadarrBlock::BlocklistClearAllItemsPrompt => { let confirmation_prompt = ConfirmationPrompt::new() @@ -64,7 +64,6 @@ impl DrawUi for BlocklistUi { .prompt("Do you want to clear your blocklist?") .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_blocklist_table(f, app, area); f.render_widget( Popup::new(confirmation_prompt).size(Size::SmallPrompt), f.area(), @@ -77,7 +76,7 @@ impl DrawUi for BlocklistUi { } fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { let current_selection = if app.data.radarr_data.blocklist.items.is_empty() { BlocklistItem::default() } else { diff --git a/src/ui/radarr_ui/collections/collection_details_ui.rs b/src/ui/radarr_ui/collections/collection_details_ui.rs index 917018c..5f5f3e1 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui.rs @@ -12,7 +12,6 @@ use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, }; use crate::models::{EnumDisplayStyle, Route}; -use crate::ui::radarr_ui::collections::draw_collections; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ borderless_block, get_width_from_percentage, layout_block_top_border_with_title, title_block, @@ -20,7 +19,7 @@ use crate::ui::utils::{ }; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::Size; -use crate::ui::{draw_popup_over, DrawUi}; +use crate::ui::{draw_popup, DrawUi}; use crate::utils::convert_runtime; #[cfg(test)] @@ -31,41 +30,25 @@ pub(super) struct CollectionDetailsUi; impl DrawUi for CollectionDetailsUi { fn accepts(route: Route) -> bool { - if let Route::Radarr(active_radarr_block, _) = route { + if let Route::Radarr(active_radarr_block, context_option) = route { + if let Some(context) = context_option { + return COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block) + && context == ActiveRadarrBlock::CollectionDetails; + } + return COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block); } false } - fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, context_option) = *app.get_current_route() { - let draw_collection_details_popup = - |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| match context_option - .unwrap_or(active_radarr_block) - { - ActiveRadarrBlock::ViewMovieOverview => { - draw_popup_over( - f, - app, - popup_area, - draw_collection_details, - draw_movie_overview, - Size::Small, - ); - } - ActiveRadarrBlock::CollectionDetails => draw_collection_details(f, app, popup_area), - _ => (), - }; + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() { + draw_popup(f, app, draw_collection_details, Size::Large); - draw_popup_over( - f, - app, - area, - draw_collections, - draw_collection_details_popup, - Size::Large, - ); + if context_option.unwrap_or(active_radarr_block) == ActiveRadarrBlock::ViewMovieOverview { + draw_popup(f, app, draw_movie_overview, Size::Small); + } } } } diff --git a/src/ui/radarr_ui/collections/collection_details_ui_tests.rs b/src/ui/radarr_ui/collections/collection_details_ui_tests.rs index 440fdf8..b52d666 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui_tests.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui_tests.rs @@ -17,5 +17,13 @@ mod tests { assert!(!CollectionDetailsUi::accepts(active_radarr_block.into())); } }); + + assert!(CollectionDetailsUi::accepts( + ( + ActiveRadarrBlock::CollectionDetails, + Some(ActiveRadarrBlock::CollectionDetails) + ) + .into() + )); } } diff --git a/src/ui/radarr_ui/collections/edit_collection_ui.rs b/src/ui/radarr_ui/collections/edit_collection_ui.rs index eac0943..765bad3 100644 --- a/src/ui/radarr_ui/collections/edit_collection_ui.rs +++ b/src/ui/radarr_ui/collections/edit_collection_ui.rs @@ -1,12 +1,10 @@ -use std::sync::atomic::Ordering; - use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::text::Text; use ratatui::widgets::{ListItem, Paragraph}; use ratatui::Frame; +use std::sync::atomic::Ordering; -use crate::app::context_clues::build_context_clue_string; -use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES; +use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::radarr::modals::EditCollectionModal; use crate::models::servarr_data::radarr::radarr_data::{ @@ -15,7 +13,6 @@ use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::{EnumDisplayStyle, Route}; use crate::render_selectable_input_box; use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsUi; -use crate::ui::radarr_ui::collections::draw_collections; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; use crate::ui::widgets::button::Button; @@ -23,7 +20,7 @@ use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::selectable_list::SelectableList; -use crate::ui::{draw_popup, draw_popup_over, draw_popup_over_ui, DrawUi}; +use crate::ui::{draw_popup, DrawUi}; #[cfg(test)] #[path = "edit_collection_ui_tests.rs"] @@ -33,51 +30,42 @@ pub(super) struct EditCollectionUi; impl DrawUi for EditCollectionUi { fn accepts(route: Route) -> bool { - if let Route::Radarr(active_radarr_block, _) = route { + if let Route::Radarr(active_radarr_block, context_option) = route { + if let Some(context) = context_option { + return EDIT_COLLECTION_BLOCKS.contains(&active_radarr_block) + && context == ActiveRadarrBlock::CollectionDetails; + } + return EDIT_COLLECTION_BLOCKS.contains(&active_radarr_block); } false } - fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, context_option) = *app.get_current_route() { - let draw_edit_collection_prompt = - |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| match active_radarr_block { - ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => { - draw_edit_collection_confirmation_prompt(f, app, prompt_area); - draw_edit_collection_select_minimum_availability_popup(f, app); - } - ActiveRadarrBlock::EditCollectionSelectQualityProfile => { - draw_edit_collection_confirmation_prompt(f, app, prompt_area); - draw_edit_collection_select_quality_profile_popup(f, app); - } - ActiveRadarrBlock::EditCollectionPrompt - | ActiveRadarrBlock::EditCollectionToggleMonitored - | ActiveRadarrBlock::EditCollectionRootFolderPathInput - | ActiveRadarrBlock::EditCollectionToggleSearchOnAdd => { - draw_edit_collection_confirmation_prompt(f, app, prompt_area) - } - _ => (), - }; - + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() { if let Some(context) = context_option { - match context { - ActiveRadarrBlock::Collections => draw_popup_over( - f, - app, - area, - draw_collections, - draw_edit_collection_prompt, - Size::Medium, - ), - _ if COLLECTION_DETAILS_BLOCKS.contains(&context) => { - draw_popup_over_ui::(f, app, area, draw_collections, Size::Large); - draw_popup(f, app, draw_edit_collection_prompt, Size::Medium); - } - _ => (), + if COLLECTION_DETAILS_BLOCKS.contains(&context) { + draw_popup(f, app, CollectionDetailsUi::draw, Size::Large); } } + + draw_popup( + f, + app, + draw_edit_collection_confirmation_prompt, + Size::Medium, + ); + + match active_radarr_block { + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => { + draw_edit_collection_select_minimum_availability_popup(f, app); + } + ActiveRadarrBlock::EditCollectionSelectQualityProfile => { + draw_edit_collection_select_quality_profile_popup(f, app); + } + _ => (), + }; } } } @@ -102,7 +90,7 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> let title = format!("Edit - {collection_title}"); let yes_no_value = app.data.radarr_data.prompt_confirm; let selected_block = app.data.radarr_data.selected_block.get_active_block(); - let highlight_yes_no = selected_block == &ActiveRadarrBlock::EditCollectionConfirmPrompt; + let highlight_yes_no = selected_block == ActiveRadarrBlock::EditCollectionConfirmPrompt; let EditCollectionModal { minimum_availability_list, quality_profile_list, @@ -135,30 +123,30 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> let help_paragraph = Paragraph::new(help_text).centered(); let prompt_paragraph = layout_paragraph_borderless(&collection_overview); let monitored_checkbox = Checkbox::new("Monitored") - .highlighted(selected_block == &ActiveRadarrBlock::EditCollectionToggleMonitored) + .highlighted(selected_block == ActiveRadarrBlock::EditCollectionToggleMonitored) .checked(monitored.unwrap_or_default()); let min_availability_drop_down_button = Button::new() .title(selected_minimum_availability.to_display_str()) .label("Minimum Availability") .icon("▼") - .selected(selected_block == &ActiveRadarrBlock::EditCollectionSelectMinimumAvailability); + .selected(selected_block == ActiveRadarrBlock::EditCollectionSelectMinimumAvailability); let quality_profile_drop_down_button = Button::new() .title(selected_quality_profile) .label("Quality Profile") .icon("▼") - .selected(selected_block == &ActiveRadarrBlock::EditCollectionSelectQualityProfile); + .selected(selected_block == ActiveRadarrBlock::EditCollectionSelectQualityProfile); - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { let root_folder_input_box = InputBox::new(&path.text) .offset(path.offset.load(Ordering::SeqCst)) .label("Root Folder") - .highlighted(selected_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput) + .highlighted(selected_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput) .selected(active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput); render_selectable_input_box!(root_folder_input_box, f, root_folder_area); } let search_on_add_checkbox = Checkbox::new("Search on Add") - .highlighted(selected_block == &ActiveRadarrBlock::EditCollectionToggleSearchOnAdd) + .highlighted(selected_block == ActiveRadarrBlock::EditCollectionToggleSearchOnAdd) .checked(search_on_add.unwrap_or_default()); let save_button = Button::new() .title("Save") diff --git a/src/ui/radarr_ui/collections/edit_collection_ui_tests.rs b/src/ui/radarr_ui/collections/edit_collection_ui_tests.rs index be9fbc4..6e37d4b 100644 --- a/src/ui/radarr_ui/collections/edit_collection_ui_tests.rs +++ b/src/ui/radarr_ui/collections/edit_collection_ui_tests.rs @@ -16,6 +16,14 @@ mod tests { } else { assert!(!EditCollectionUi::accepts(active_radarr_block.into())); } - }) + }); + + assert!(EditCollectionUi::accepts( + ( + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::CollectionDetails) + ) + .into() + )); } } diff --git a/src/ui/radarr_ui/collections/mod.rs b/src/ui/radarr_ui/collections/mod.rs index c3fa10b..37269f6 100644 --- a/src/ui/radarr_ui/collections/mod.rs +++ b/src/ui/radarr_ui/collections/mod.rs @@ -2,8 +2,6 @@ use ratatui::layout::{Constraint, Rect}; use ratatui::widgets::{Cell, Row}; use ratatui::Frame; -pub(super) use collection_details_ui::draw_collection_details; - use crate::app::App; use crate::models::radarr_models::Collection; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, COLLECTIONS_BLOCKS}; @@ -14,9 +12,8 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; -use crate::ui::widgets::message::Message; use crate::ui::widgets::popup::{Popup, Size}; -use crate::ui::{draw_input_box_popup, draw_popup_over, DrawUi}; +use crate::ui::DrawUi; mod collection_details_ui; #[cfg(test)] @@ -38,61 +35,22 @@ impl DrawUi for CollectionsUi { } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let route = *app.get_current_route(); - let mut collections_ui_matcher = |active_radarr_block| match active_radarr_block { - ActiveRadarrBlock::Collections | ActiveRadarrBlock::CollectionsSortPrompt => { - draw_collections(f, app, area) - } - ActiveRadarrBlock::SearchCollection => draw_popup_over( - f, - app, - area, - draw_collections, - draw_collection_search_box, - Size::InputBox, - ), - ActiveRadarrBlock::SearchCollectionError => { - let popup = Popup::new(Message::new("Collection not found!")).size(Size::Message); + let route = app.get_current_route(); + draw_collections(f, app, area); - draw_collections(f, app, area); - f.render_widget(popup, f.area()); - } - ActiveRadarrBlock::FilterCollections => draw_popup_over( - f, - app, - area, - draw_collections, - draw_filter_collections_box, - Size::InputBox, - ), - ActiveRadarrBlock::FilterCollectionsError => { - let popup = Popup::new(Message::new( - "No collections found matching the given filter!", - )) - .size(Size::Message); - - draw_collections(f, app, area); - f.render_widget(popup, f.area()); - } - ActiveRadarrBlock::UpdateAllCollectionsPrompt => { + match route { + _ if CollectionDetailsUi::accepts(route) => CollectionDetailsUi::draw(f, app, area), + _ if EditCollectionUi::accepts(route) => EditCollectionUi::draw(f, app, area), + Route::Radarr(ActiveRadarrBlock::UpdateAllCollectionsPrompt, _) => { let confirmation_prompt = ConfirmationPrompt::new() .title("Update All Collections") .prompt("Do you want to update all of your collections?") .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_collections(f, app, area); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); - } - _ => (), - }; - - match route { - _ if CollectionDetailsUi::accepts(route) => CollectionDetailsUi::draw(f, app, area), - _ if EditCollectionUi::accepts(route) => EditCollectionUi::draw(f, app, area), - Route::Radarr(active_radarr_block, _) - if COLLECTIONS_BLOCKS.contains(&active_radarr_block) => - { - collections_ui_matcher(active_radarr_block) + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } _ => (), } @@ -100,7 +58,7 @@ impl DrawUi for CollectionsUi { } pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { let current_selection = if !app.data.radarr_data.collections.items.is_empty() { app.data.radarr_data.collections.current_selection().clone() } else { @@ -149,10 +107,22 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .primary() }; let collections_table = ManagarrTable::new(content, collection_row_mapping) - .loading(app.is_loading) + .loading( + app.is_loading + || app.data.radarr_data.movies.is_empty() + || app.data.radarr_data.quality_profile_map.is_empty(), + ) .footer(collections_table_footer) .block(layout_block_top_border()) .sorting(active_radarr_block == ActiveRadarrBlock::CollectionsSortPrompt) + .searching(active_radarr_block == ActiveRadarrBlock::SearchCollection) + .search_produced_empty_results( + active_radarr_block == ActiveRadarrBlock::SearchCollectionError, + ) + .filtering(active_radarr_block == ActiveRadarrBlock::FilterCollections) + .filter_produced_empty_results( + active_radarr_block == ActiveRadarrBlock::FilterCollectionsError, + ) .headers([ "Collection", "Number of Movies", @@ -170,24 +140,15 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) Constraint::Percentage(15), ]); + if [ + ActiveRadarrBlock::SearchCollection, + ActiveRadarrBlock::FilterCollections, + ] + .contains(&active_radarr_block) + { + collections_table.show_cursor(f, area); + } + f.render_widget(collections_table, area); } } - -fn draw_collection_search_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_input_box_popup( - f, - area, - "Search", - app.data.radarr_data.collections.search.as_ref().unwrap(), - ); -} - -fn draw_filter_collections_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_input_box_popup( - f, - area, - "Filter", - app.data.radarr_data.collections.filter.as_ref().unwrap(), - ) -} diff --git a/src/ui/radarr_ui/downloads/mod.rs b/src/ui/radarr_ui/downloads/mod.rs index fa17417..d247ff8 100644 --- a/src/ui/radarr_ui/downloads/mod.rs +++ b/src/ui/radarr_ui/downloads/mod.rs @@ -30,9 +30,10 @@ impl DrawUi for DownloadsUi { } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { + draw_downloads(f, app, area); + match active_radarr_block { - ActiveRadarrBlock::Downloads => draw_downloads(f, app, area), ActiveRadarrBlock::DeleteDownloadPrompt => { let prompt = format!( "Do you really want to delete this download: \n{}?", @@ -43,8 +44,10 @@ impl DrawUi for DownloadsUi { .prompt(&prompt) .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_downloads(f, app, area); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } ActiveRadarrBlock::UpdateDownloadsPrompt => { let confirmation_prompt = ConfirmationPrompt::new() @@ -52,8 +55,10 @@ impl DrawUi for DownloadsUi { .prompt("Do you want to update your downloads?") .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_downloads(f, app, area); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } _ => (), } diff --git a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs index b185adb..68724e2 100644 --- a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs +++ b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs @@ -1,12 +1,10 @@ use std::sync::atomic::Ordering; -use crate::app::context_clues::build_context_clue_string; -use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES; +use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::Route; use crate::render_selectable_input_box; -use crate::ui::radarr_ui::indexers::draw_indexers; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; use crate::ui::widgets::button::Button; @@ -14,7 +12,7 @@ use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::popup::Size; -use crate::ui::{draw_popup_over, DrawUi}; +use crate::ui::{draw_popup, DrawUi}; use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::text::Text; use ratatui::widgets::Paragraph; @@ -35,15 +33,8 @@ impl DrawUi for EditIndexerUi { false } - fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_popup_over( - f, - app, - area, - draw_indexers, - draw_edit_indexer_prompt, - Size::LargePrompt, - ); + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_edit_indexer_prompt, Size::WideLargePrompt); } } @@ -51,7 +42,7 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let block = title_block_centered("Edit Indexer"); let yes_no_value = app.data.radarr_data.prompt_confirm; let selected_block = app.data.radarr_data.selected_block.get_active_block(); - let highlight_yes_no = selected_block == &ActiveRadarrBlock::EditIndexerConfirmPrompt; + let highlight_yes_no = selected_block == ActiveRadarrBlock::EditIndexerConfirmPrompt; let edit_indexer_modal_option = &app.data.radarr_data.edit_indexer_modal; let protocol = &app.data.radarr_data.indexers.current_selection().protocol; let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); @@ -60,8 +51,9 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if edit_indexer_modal_option.is_some() { let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap(); - let [settings_area, _, buttons_area, help_area] = Layout::vertical([ - Constraint::Length(15), + let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(18), Constraint::Fill(1), Constraint::Length(3), Constraint::Length(1), @@ -72,13 +64,15 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .margin(1) .areas(settings_area); - let [name_area, rss_area, auto_search_area, interactive_search_area] = Layout::vertical([ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - ]) - .areas(left_side_area); + let [name_area, rss_area, auto_search_area, interactive_search_area, priority_area] = + Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ]) + .areas(left_side_area); let [url_area, api_key_area, seed_ratio_area, tags_area] = Layout::vertical([ Constraint::Length(3), Constraint::Length(3), @@ -87,27 +81,33 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .areas(right_side_area); - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { + let priority = edit_indexer_modal.priority.to_string(); let name_input_box = InputBox::new(&edit_indexer_modal.name.text) .offset(edit_indexer_modal.name.offset.load(Ordering::SeqCst)) .label("Name") - .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerNameInput) + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerNameInput) .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerNameInput); let url_input_box = InputBox::new(&edit_indexer_modal.url.text) .offset(edit_indexer_modal.url.offset.load(Ordering::SeqCst)) .label("URL") - .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerUrlInput) + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerUrlInput) .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerUrlInput); let api_key_input_box = InputBox::new(&edit_indexer_modal.api_key.text) .offset(edit_indexer_modal.api_key.offset.load(Ordering::SeqCst)) .label("API Key") - .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerApiKeyInput) + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerApiKeyInput) .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerApiKeyInput); let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) .offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst)) .label("Tags") - .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerTagsInput) + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerTagsInput) .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput); + let priority_input_box = InputBox::new(&priority) + .cursor_after_string(false) + .label("Indexer Priority ▴▾") + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerPriorityInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerPriorityInput); render_selectable_input_box!(name_input_box, f, name_area); render_selectable_input_box!(url_input_box, f, url_area); @@ -117,39 +117,39 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let seed_ratio_input_box = InputBox::new(&edit_indexer_modal.seed_ratio.text) .offset(edit_indexer_modal.seed_ratio.offset.load(Ordering::SeqCst)) .label("Seed Ratio") - .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerSeedRatioInput) + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerSeedRatioInput) .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerSeedRatioInput); let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) .offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst)) .label("Tags") - .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerTagsInput) + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerTagsInput) .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput); render_selectable_input_box!(seed_ratio_input_box, f, seed_ratio_area); render_selectable_input_box!(tags_input_box, f, tags_area); + render_selectable_input_box!(priority_input_box, f, priority_area); } else { render_selectable_input_box!(tags_input_box, f, seed_ratio_area); + render_selectable_input_box!(priority_input_box, f, tags_area); } let rss_checkbox = Checkbox::new("Enable RSS") .checked(edit_indexer_modal.enable_rss.unwrap_or_default()) - .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableRss); + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerToggleEnableRss); let auto_search_checkbox = Checkbox::new("Enable Automatic Search") .checked( edit_indexer_modal .enable_automatic_search .unwrap_or_default(), ) - .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch); + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch); let interactive_search_checkbox = Checkbox::new("Enable Interactive Search") .checked( edit_indexer_modal .enable_interactive_search .unwrap_or_default(), ) - .highlighted( - selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, - ); + .highlighted(selected_block == ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch); let [save_area, cancel_area] = Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) diff --git a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs index a9bc0ea..3dd1972 100644 --- a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs +++ b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs @@ -5,15 +5,13 @@ use ratatui::text::Text; use ratatui::widgets::Paragraph; use ratatui::Frame; -use crate::app::context_clues::build_context_clue_string; -use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES; +use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS, }; use crate::models::Route; use crate::render_selectable_input_box; -use crate::ui::radarr_ui::indexers::draw_indexers; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; use crate::ui::widgets::button::Button; @@ -21,7 +19,7 @@ use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::popup::Size; -use crate::ui::{draw_popup_over, DrawUi}; +use crate::ui::{draw_popup, DrawUi}; #[cfg(test)] #[path = "indexer_settings_ui_tests.rs"] @@ -38,14 +36,12 @@ impl DrawUi for IndexerSettingsUi { false } - fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_popup_over( + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup( f, app, - area, - draw_indexers, draw_edit_indexer_settings_prompt, - Size::LargePrompt, + Size::WideLargePrompt, ); } } @@ -54,7 +50,7 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: let block = title_block_centered("Configure All Indexer Settings"); let yes_no_value = app.data.radarr_data.prompt_confirm; let selected_block = app.data.radarr_data.selected_block.get_active_block(); - let highlight_yes_no = selected_block == &ActiveRadarrBlock::IndexerSettingsConfirmPrompt; + let highlight_yes_no = selected_block == ActiveRadarrBlock::IndexerSettingsConfirmPrompt; let indexer_settings_option = &app.data.radarr_data.indexer_settings; let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); let help_paragraph = Paragraph::new(help_text).centered(); @@ -62,7 +58,8 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: if indexer_settings_option.is_some() { let indexer_settings = indexer_settings_option.as_ref().unwrap(); - let [settings_area, _, buttons_area, help_area] = Layout::vertical([ + let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([ + Constraint::Fill(1), Constraint::Length(15), Constraint::Fill(1), Constraint::Length(3), @@ -90,7 +87,7 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: ]) .areas(right_side_area); - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { let min_age = indexer_settings.minimum_age.to_string(); let retention = indexer_settings.retention.to_string(); let max_size = indexer_settings.maximum_size.to_string(); @@ -100,27 +97,27 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: let min_age_text_box = InputBox::new(&min_age) .cursor_after_string(false) .label("Minimum Age (minutes) ▴▾") - .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput) + .highlighted(selected_block == ActiveRadarrBlock::IndexerSettingsMinimumAgeInput) .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsMinimumAgeInput); let retention_input_box = InputBox::new(&retention) .cursor_after_string(false) .label("Retention (days) ▴▾") - .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsRetentionInput) + .highlighted(selected_block == ActiveRadarrBlock::IndexerSettingsRetentionInput) .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsRetentionInput); let max_size_input_box = InputBox::new(&max_size) .cursor_after_string(false) .label("Maximum Size (MB) ▴▾") - .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput) + .highlighted(selected_block == ActiveRadarrBlock::IndexerSettingsMaximumSizeInput) .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsMaximumSizeInput); let availability_delay_input_box = InputBox::new(&availability_delay) .cursor_after_string(false) .label("Availability Delay (days) ▴▾") - .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput) + .highlighted(selected_block == ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput) .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput); let rss_sync_interval_input_box = InputBox::new(&rss_sync_interval) .cursor_after_string(false) .label("RSS Sync Interval (minutes) ▴▾") - .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput) + .highlighted(selected_block == ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput) .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput); let whitelisted_subs_input_box = InputBox::new(&indexer_settings.whitelisted_hardcoded_subs.text) @@ -132,7 +129,7 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: ) .label("Whitelisted Subtitle Tags") .highlighted( - selected_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + selected_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, ) .selected( active_radarr_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, @@ -147,10 +144,10 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: } let prefer_indexer_flags_checkbox = Checkbox::new("Prefer Indexer Flags") - .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags) + .highlighted(selected_block == ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags) .checked(indexer_settings.prefer_indexer_flags); let allow_hardcoded_subs_checkbox = Checkbox::new("Allow Hardcoded Subs") - .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs) + .highlighted(selected_block == ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs) .checked(indexer_settings.allow_hardcoded_subs); let [save_area, cancel_area] = diff --git a/src/ui/radarr_ui/indexers/mod.rs b/src/ui/radarr_ui/indexers/mod.rs index d79c84f..93aad8a 100644 --- a/src/ui/radarr_ui/indexers/mod.rs +++ b/src/ui/radarr_ui/indexers/mod.rs @@ -43,61 +43,68 @@ impl DrawUi for IndexersUi { } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let route = *app.get_current_route(); - let mut indexers_matchers = |active_radarr_block| match active_radarr_block { - ActiveRadarrBlock::Indexers => draw_indexers(f, app, area), - ActiveRadarrBlock::TestIndexer => { - draw_indexers(f, app, area); - if app.is_loading { - let loading_popup = Popup::new(LoadingBlock::new( - app.is_loading, - title_block("Testing Indexer"), - )) - .size(Size::LargeMessage); - f.render_widget(loading_popup, f.area()); - } else { - let popup = if let Some(result) = app.data.radarr_data.indexer_test_error.as_ref() { - Popup::new(Message::new(result.clone())).size(Size::LargeMessage) - } else { - let message = Message::new("Indexer test succeeded!") - .title("Success") - .style(Style::new().success().bold()); - Popup::new(message).size(Size::Message) - }; - - f.render_widget(popup, f.area()); - } - } - ActiveRadarrBlock::DeleteIndexerPrompt => { - let prompt = format!( - "Do you really want to delete this indexer: \n{}?", - app - .data - .radarr_data - .indexers - .current_selection() - .name - .clone() - .unwrap_or_default() - ); - let confirmation_prompt = ConfirmationPrompt::new() - .title("Delete Indexer") - .prompt(&prompt) - .yes_no_value(app.data.radarr_data.prompt_confirm); - - draw_indexers(f, app, area); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); - } - _ => (), - }; + let route = app.get_current_route(); + draw_indexers(f, app, area); match route { _ if EditIndexerUi::accepts(route) => EditIndexerUi::draw(f, app, area), _ if IndexerSettingsUi::accepts(route) => IndexerSettingsUi::draw(f, app, area), _ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area), - Route::Radarr(active_radarr_block, _) if INDEXERS_BLOCKS.contains(&active_radarr_block) => { - indexers_matchers(active_radarr_block) - } + Route::Radarr(active_radarr_block, _) => match active_radarr_block { + ActiveRadarrBlock::TestIndexer => { + if app.is_loading || app.data.radarr_data.indexer_test_errors.is_none() { + let loading_popup = Popup::new(LoadingBlock::new( + app.is_loading || app.data.radarr_data.indexer_test_errors.is_none(), + title_block("Testing Indexer"), + )) + .size(Size::LargeMessage); + f.render_widget(loading_popup, f.area()); + } else { + let popup = { + let result = app + .data + .radarr_data + .indexer_test_errors + .as_ref() + .expect("Test result is unpopulated"); + + if !result.is_empty() { + Popup::new(Message::new(result.clone())).size(Size::LargeMessage) + } else { + let message = Message::new("Indexer test succeeded!") + .title("Success") + .style(Style::new().success().bold()); + Popup::new(message).size(Size::Message) + } + }; + + f.render_widget(popup, f.area()); + } + } + ActiveRadarrBlock::DeleteIndexerPrompt => { + let prompt = format!( + "Do you really want to delete this indexer: \n{}?", + app + .data + .radarr_data + .indexers + .current_selection() + .name + .clone() + .unwrap_or_default() + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Indexer") + .prompt(&prompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + }, _ => (), } } diff --git a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs index 1b36a45..f7adc0d 100644 --- a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs @@ -3,12 +3,11 @@ use crate::app::App; use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::Route; -use crate::ui::radarr_ui::indexers::draw_indexers; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{borderless_block, get_width_from_percentage, title_block}; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::Size; -use crate::ui::{draw_popup_over, DrawUi}; +use crate::ui::{draw_popup, DrawUi}; use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::widgets::{Cell, Row}; use ratatui::Frame; @@ -28,26 +27,22 @@ impl DrawUi for TestAllIndexersUi { false } - fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_popup_over( - f, - app, - area, - draw_indexers, - draw_test_all_indexers_test_results, - Size::Large, - ); + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_test_all_indexers_test_results, Size::Large); } } fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let is_loading = app.is_loading || app.data.radarr_data.indexer_test_all_results.is_none(); + let block = title_block("Test All Indexers"); + let current_selection = if let Some(test_all_results) = app.data.radarr_data.indexer_test_all_results.as_ref() { test_all_results.current_selection().clone() } else { IndexerTestResultModalItem::default() }; - f.render_widget(title_block("Test All Indexers"), area); + f.render_widget(block, area); let help_footer = format!( "<↑↓> scroll | {}", build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) @@ -77,7 +72,7 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are test_results_row_mapping, ) .block(borderless_block()) - .loading(app.is_loading) + .loading(is_loading) .footer(Some(help_footer)) .footer_alignment(Alignment::Center) .margin(1) diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index 6903a6a..c3670f9 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -5,16 +5,15 @@ use ratatui::text::Text; use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; use ratatui::Frame; -use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES}; -use crate::app::radarr::radarr_context_clues::{ - ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, +use crate::app::context_clues::{ + build_context_clue_string, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, }; +use crate::app::radarr::radarr_context_clues::ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES; use crate::models::radarr_models::AddMovieSearchResult; use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS}; use crate::models::{EnumDisplayStyle, Route}; -use crate::ui::radarr_ui::collections::{draw_collection_details, draw_collections}; -use crate::ui::radarr_ui::library::draw_library; +use crate::ui::radarr_ui::collections::CollectionsUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ borderless_block, get_width_from_percentage, layout_block, layout_paragraph_borderless, @@ -26,7 +25,7 @@ use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::message::Message; use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::selectable_list::SelectableList; -use crate::ui::{draw_popup_over, DrawUi}; +use crate::ui::{draw_popup, DrawUi}; use crate::utils::convert_runtime; use crate::{render_selectable_input_box, App}; @@ -46,73 +45,30 @@ impl DrawUi for AddMovieUi { } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, context_option) = *app.get_current_route() { - let draw_add_movie_search_popup = - |f: &mut Frame<'_>, app: &mut App<'_>, area: Rect| match active_radarr_block { - ActiveRadarrBlock::AddMovieSearchInput - | ActiveRadarrBlock::AddMovieSearchResults - | ActiveRadarrBlock::AddMovieEmptySearchResults => { - draw_add_movie_search(f, app, area); - } + if let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() { + if context_option.is_some() { + CollectionsUi::draw(f, app, area); + draw_popup(f, app, draw_confirmation_popup, Size::Medium); + } else { + draw_popup(f, app, draw_add_movie_search, Size::Large); + + match active_radarr_block { ActiveRadarrBlock::AddMoviePrompt | ActiveRadarrBlock::AddMovieSelectMonitor | ActiveRadarrBlock::AddMovieSelectMinimumAvailability | ActiveRadarrBlock::AddMovieSelectQualityProfile | ActiveRadarrBlock::AddMovieSelectRootFolder | ActiveRadarrBlock::AddMovieTagsInput => { - if context_option.is_some() { - draw_popup_over( - f, - app, - area, - draw_collection_details, - draw_confirmation_popup, - Size::Medium, - ); - } else { - draw_popup_over( - f, - app, - area, - draw_add_movie_search, - draw_confirmation_popup, - Size::Medium, - ); - } + draw_popup(f, app, draw_confirmation_popup, Size::Medium); } ActiveRadarrBlock::AddMovieAlreadyInLibrary => { - draw_add_movie_search(f, app, area); f.render_widget( Popup::new(Message::new("This film is already in your library")).size(Size::Message), f.area(), ); } _ => (), - }; - - match active_radarr_block { - _ if ADD_MOVIE_BLOCKS.contains(&active_radarr_block) => { - if context_option.is_some() { - draw_popup_over( - f, - app, - area, - draw_collections, - draw_add_movie_search_popup, - Size::Large, - ) - } else { - draw_popup_over( - f, - app, - area, - draw_library, - draw_add_movie_search_popup, - Size::Large, - ) - } } - _ => (), } } } @@ -202,7 +158,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .primary() }; - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { match active_radarr_block { ActiveRadarrBlock::AddMovieSearchInput => { let search_box = InputBox::new(block_content) @@ -284,27 +240,22 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } fn draw_confirmation_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { + draw_confirmation_prompt(f, app, area); + match active_radarr_block { ActiveRadarrBlock::AddMovieSelectMonitor => { - draw_confirmation_prompt(f, app, area); draw_add_movie_select_monitor_popup(f, app); } ActiveRadarrBlock::AddMovieSelectMinimumAvailability => { - draw_confirmation_prompt(f, app, area); draw_add_movie_select_minimum_availability_popup(f, app); } ActiveRadarrBlock::AddMovieSelectQualityProfile => { - draw_confirmation_prompt(f, app, area); draw_add_movie_select_quality_profile_popup(f, app); } ActiveRadarrBlock::AddMovieSelectRootFolder => { - draw_confirmation_prompt(f, app, area); draw_add_movie_select_root_folder_popup(f, app); } - ActiveRadarrBlock::AddMoviePrompt | ActiveRadarrBlock::AddMovieTagsInput => { - draw_confirmation_prompt(f, app, area) - } _ => (), } } @@ -354,7 +305,7 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let prompt = movie_overview; let yes_no_value = app.data.radarr_data.prompt_confirm; let selected_block = app.data.radarr_data.selected_block.get_active_block(); - let highlight_yes_no = selected_block == &ActiveRadarrBlock::AddMovieConfirmPrompt; + let highlight_yes_no = selected_block == ActiveRadarrBlock::AddMovieConfirmPrompt; let AddMovieModal { monitor_list, minimum_availability_list, @@ -400,33 +351,33 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .title(&selected_root_folder.path) .label("Root Folder") .icon("▼") - .selected(selected_block == &ActiveRadarrBlock::AddMovieSelectRootFolder); + .selected(selected_block == ActiveRadarrBlock::AddMovieSelectRootFolder); let monitor_drop_down_button = Button::new() .title(selected_monitor.to_display_str()) .label("Monitor") .icon("▼") - .selected(selected_block == &ActiveRadarrBlock::AddMovieSelectMonitor); + .selected(selected_block == ActiveRadarrBlock::AddMovieSelectMonitor); let min_availability_drop_down_button = Button::new() .title(selected_minimum_availability.to_display_str()) .label("Minimum Availability") .icon("▼") - .selected(selected_block == &ActiveRadarrBlock::AddMovieSelectMinimumAvailability); + .selected(selected_block == ActiveRadarrBlock::AddMovieSelectMinimumAvailability); let quality_profile_drop_down_button = Button::new() .title(selected_quality_profile) .label("Quality Profile") .icon("▼") - .selected(selected_block == &ActiveRadarrBlock::AddMovieSelectQualityProfile); + .selected(selected_block == ActiveRadarrBlock::AddMovieSelectQualityProfile); f.render_widget(root_folder_drop_down_button, root_folder_area); f.render_widget(monitor_drop_down_button, monitor_area); f.render_widget(min_availability_drop_down_button, min_availability_area); f.render_widget(quality_profile_drop_down_button, quality_profile_area); - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { let tags_input_box = InputBox::new(&tags.text) .offset(tags.offset.load(Ordering::SeqCst)) .label("Tags") - .highlighted(selected_block == &ActiveRadarrBlock::AddMovieTagsInput) + .highlighted(selected_block == ActiveRadarrBlock::AddMovieTagsInput) .selected(active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput); render_selectable_input_box!(tags_input_box, f, tags_area); } diff --git a/src/ui/radarr_ui/library/delete_movie_ui.rs b/src/ui/radarr_ui/library/delete_movie_ui.rs index 3dd0d61..9a338da 100644 --- a/src/ui/radarr_ui/library/delete_movie_ui.rs +++ b/src/ui/radarr_ui/library/delete_movie_ui.rs @@ -4,7 +4,6 @@ use ratatui::Frame; use crate::app::App; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS}; use crate::models::Route; -use crate::ui::radarr_ui::library::draw_library; use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::popup::{Popup, Size}; @@ -25,9 +24,9 @@ impl DrawUi for DeleteMovieUi { false } - fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { if matches!( - *app.get_current_route(), + app.get_current_route(), Route::Radarr(ActiveRadarrBlock::DeleteMoviePrompt, _) ) { let selected_block = app.data.radarr_data.selected_block.get_active_block(); @@ -38,20 +37,22 @@ impl DrawUi for DeleteMovieUi { let checkboxes = vec![ Checkbox::new("Delete Movie File") .checked(app.data.radarr_data.delete_movie_files) - .highlighted(selected_block == &ActiveRadarrBlock::DeleteMovieToggleDeleteFile), + .highlighted(selected_block == ActiveRadarrBlock::DeleteMovieToggleDeleteFile), Checkbox::new("Add List Exclusion") .checked(app.data.radarr_data.add_list_exclusion) - .highlighted(selected_block == &ActiveRadarrBlock::DeleteMovieToggleAddListExclusion), + .highlighted(selected_block == ActiveRadarrBlock::DeleteMovieToggleAddListExclusion), ]; let confirmation_prompt = ConfirmationPrompt::new() .title("Delete Movie") .prompt(&prompt) .checkboxes(checkboxes) - .yes_no_highlighted(selected_block == &ActiveRadarrBlock::DeleteMovieConfirmPrompt) + .yes_no_highlighted(selected_block == ActiveRadarrBlock::DeleteMovieConfirmPrompt) .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_library(f, app, area); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } } } diff --git a/src/ui/radarr_ui/library/edit_movie_ui.rs b/src/ui/radarr_ui/library/edit_movie_ui.rs index 520bf2d..09ba302 100644 --- a/src/ui/radarr_ui/library/edit_movie_ui.rs +++ b/src/ui/radarr_ui/library/edit_movie_ui.rs @@ -6,8 +6,7 @@ use ratatui::text::Text; use ratatui::widgets::{ListItem, Paragraph}; use ratatui::Frame; -use crate::app::context_clues::build_context_clue_string; -use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES; +use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ @@ -15,7 +14,6 @@ use crate::models::servarr_data::radarr::radarr_data::{ }; use crate::models::{EnumDisplayStyle, Route}; use crate::render_selectable_input_box; -use crate::ui::radarr_ui::library::draw_library; use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi; use crate::ui::styles::ManagarrStyle; @@ -25,7 +23,7 @@ use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::selectable_list::SelectableList; -use crate::ui::{draw_popup, draw_popup_over, draw_popup_over_ui, DrawUi}; +use crate::ui::{draw_popup, DrawUi}; #[cfg(test)] #[path = "edit_movie_ui_tests.rs"] @@ -42,46 +40,25 @@ impl DrawUi for EditMovieUi { false } - fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, context_option) = *app.get_current_route() { - let draw_edit_movie_prompt = - |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| match active_radarr_block { - ActiveRadarrBlock::EditMovieSelectMinimumAvailability => { - draw_edit_movie_confirmation_prompt(f, app, prompt_area); - draw_edit_movie_select_minimum_availability_popup(f, app); - } - ActiveRadarrBlock::EditMovieSelectQualityProfile => { - draw_edit_movie_confirmation_prompt(f, app, prompt_area); - draw_edit_movie_select_quality_profile_popup(f, app); - } - ActiveRadarrBlock::EditMoviePrompt - | ActiveRadarrBlock::EditMovieToggleMonitored - | ActiveRadarrBlock::EditMoviePathInput - | ActiveRadarrBlock::EditMovieTagsInput => { - draw_edit_movie_confirmation_prompt(f, app, prompt_area) - } - _ => (), - }; - + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() { if let Some(context) = context_option { - match context { - ActiveRadarrBlock::Movies => { - draw_popup_over( - f, - app, - area, - draw_library, - draw_edit_movie_prompt, - Size::Medium, - ); - } - _ if MOVIE_DETAILS_BLOCKS.contains(&context) => { - draw_popup_over_ui::(f, app, area, draw_library, Size::Large); - draw_popup(f, app, draw_edit_movie_prompt, Size::Medium); - } - _ => (), + if MOVIE_DETAILS_BLOCKS.contains(&context) { + draw_popup(f, app, MovieDetailsUi::draw, Size::Large); } } + + draw_popup(f, app, draw_edit_movie_confirmation_prompt, Size::Medium); + + match active_radarr_block { + ActiveRadarrBlock::EditMovieSelectMinimumAvailability => { + draw_edit_movie_select_minimum_availability_popup(f, app); + } + ActiveRadarrBlock::EditMovieSelectQualityProfile => { + draw_edit_movie_select_quality_profile_popup(f, app); + } + _ => (), + } } } } @@ -105,7 +82,7 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are let title = format!("Edit - {movie_title}"); let yes_no_value = app.data.radarr_data.prompt_confirm; let selected_block = app.data.radarr_data.selected_block.get_active_block(); - let highlight_yes_no = selected_block == &ActiveRadarrBlock::EditMovieConfirmPrompt; + let highlight_yes_no = selected_block == ActiveRadarrBlock::EditMovieConfirmPrompt; let EditMovieModal { minimum_availability_list, quality_profile_list, @@ -139,28 +116,28 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are let prompt_paragraph = layout_paragraph_borderless(&movie_overview); let monitored_checkbox = Checkbox::new("Monitored") .checked(monitored.unwrap_or_default()) - .highlighted(selected_block == &ActiveRadarrBlock::EditMovieToggleMonitored); + .highlighted(selected_block == ActiveRadarrBlock::EditMovieToggleMonitored); let min_availability_drop_down_button = Button::new() .title(selected_minimum_availability.to_display_str()) .label("Minimum Availability") .icon("▼") - .selected(selected_block == &ActiveRadarrBlock::EditMovieSelectMinimumAvailability); + .selected(selected_block == ActiveRadarrBlock::EditMovieSelectMinimumAvailability); let quality_profile_drop_down_button = Button::new() .title(selected_quality_profile) .label("Quality Profile") .icon("▼") - .selected(selected_block == &ActiveRadarrBlock::EditMovieSelectQualityProfile); + .selected(selected_block == ActiveRadarrBlock::EditMovieSelectQualityProfile); - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { let path_input_box = InputBox::new(&path.text) .offset(path.offset.load(Ordering::SeqCst)) .label("Path") - .highlighted(selected_block == &ActiveRadarrBlock::EditMoviePathInput) + .highlighted(selected_block == ActiveRadarrBlock::EditMoviePathInput) .selected(active_radarr_block == ActiveRadarrBlock::EditMoviePathInput); let tags_input_box = InputBox::new(&tags.text) .offset(tags.offset.load(Ordering::SeqCst)) .label("Tags") - .highlighted(selected_block == &ActiveRadarrBlock::EditMovieTagsInput) + .highlighted(selected_block == ActiveRadarrBlock::EditMovieTagsInput) .selected(active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput); match active_radarr_block { diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index e34b443..3afc071 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -14,9 +14,8 @@ use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; -use crate::ui::widgets::message::Message; use crate::ui::widgets::popup::{Popup, Size}; -use crate::ui::{draw_input_box_popup, draw_popup_over, DrawUi}; +use crate::ui::DrawUi; use crate::utils::{convert_runtime, convert_to_gb}; mod add_movie_ui; @@ -44,66 +43,32 @@ impl DrawUi for LibraryUi { } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let route = *app.get_current_route(); - let mut library_ui_matchers = |active_radarr_block: ActiveRadarrBlock| match active_radarr_block - { - ActiveRadarrBlock::Movies | ActiveRadarrBlock::MoviesSortPrompt => draw_library(f, app, area), - ActiveRadarrBlock::SearchMovie => draw_popup_over( - f, - app, - area, - draw_library, - draw_movie_search_box, - Size::InputBox, - ), - ActiveRadarrBlock::SearchMovieError => { - let popup = Popup::new(Message::new("Movie not found!")).size(Size::Message); - - draw_library(f, app, area); - f.render_widget(popup, f.area()); - } - ActiveRadarrBlock::FilterMovies => draw_popup_over( - f, - app, - area, - draw_library, - draw_filter_movies_box, - Size::InputBox, - ), - ActiveRadarrBlock::FilterMoviesError => { - let popup = Popup::new(Message::new("No movies found matching the given filter!")) - .size(Size::Message); - - draw_library(f, app, area); - f.render_widget(popup, f.area()); - } - ActiveRadarrBlock::UpdateAllMoviesPrompt => { - let confirmation_prompt = ConfirmationPrompt::new() - .title("Update All Movies") - .prompt("Do you want to update info and scan your disks for all of your movies?") - .yes_no_value(app.data.radarr_data.prompt_confirm); - - draw_library(f, app, area); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); - } - _ => (), - }; + let route = app.get_current_route(); + draw_library(f, app, area); match route { _ if MovieDetailsUi::accepts(route) => MovieDetailsUi::draw(f, app, area), _ if AddMovieUi::accepts(route) => AddMovieUi::draw(f, app, area), _ if EditMovieUi::accepts(route) => EditMovieUi::draw(f, app, area), _ if DeleteMovieUi::accepts(route) => DeleteMovieUi::draw(f, app, area), - Route::Radarr(active_radarr_block, _) if LIBRARY_BLOCKS.contains(&active_radarr_block) => { - library_ui_matchers(active_radarr_block) + Route::Radarr(ActiveRadarrBlock::UpdateAllMoviesPrompt, _) => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update All Movies") + .prompt("Do you want to update info and scan your disks for all of your movies?") + .yes_no_value(app.data.radarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } _ => (), } } } -pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { +fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { let current_selection = if !app.data.radarr_data.movies.items.is_empty() { app.data.radarr_data.movies.current_selection().clone() } else { @@ -171,6 +136,10 @@ pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .loading(app.is_loading) .footer(help_footer) .sorting(active_radarr_block == ActiveRadarrBlock::MoviesSortPrompt) + .searching(active_radarr_block == ActiveRadarrBlock::SearchMovie) + .search_produced_empty_results(active_radarr_block == ActiveRadarrBlock::SearchMovieError) + .filtering(active_radarr_block == ActiveRadarrBlock::FilterMovies) + .filter_produced_empty_results(active_radarr_block == ActiveRadarrBlock::FilterMoviesError) .headers([ "Title", "Year", @@ -196,24 +165,15 @@ pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Constraint::Percentage(12), ]); + if [ + ActiveRadarrBlock::SearchMovie, + ActiveRadarrBlock::FilterMovies, + ] + .contains(&active_radarr_block) + { + library_table.show_cursor(f, area); + } + f.render_widget(library_table, area); } } - -fn draw_movie_search_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_input_box_popup( - f, - area, - "Search", - app.data.radarr_data.movies.search.as_ref().unwrap(), - ); -} - -fn draw_filter_movies_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_input_box_popup( - f, - area, - "Filter", - app.data.radarr_data.movies.filter.as_ref().unwrap(), - ) -} diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index ff7dd77..b9dd2af 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -11,16 +11,16 @@ use crate::models::radarr_models::{Credit, MovieHistoryItem, RadarrRelease}; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS}; use crate::models::Route; -use crate::ui::radarr_ui::library::draw_library; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ - borderless_block, get_width_from_percentage, layout_block_bottom_border, layout_block_top_border, + borderless_block, decorate_peer_style, get_width_from_percentage, layout_block_bottom_border, + layout_block_top_border, }; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::{Popup, Size}; -use crate::ui::{draw_popup_over, draw_tabs, DrawUi}; +use crate::ui::{draw_popup, draw_tabs, DrawUi}; use crate::utils::convert_to_gb; #[cfg(test)] @@ -38,8 +38,8 @@ impl DrawUi for MovieDetailsUi { false } - fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, context_option) = *app.get_current_route() { + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() { let draw_movie_info_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| { let content_area = draw_tabs( f, @@ -60,8 +60,10 @@ impl DrawUi for MovieDetailsUi { .prompt(&prompt) .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_movie_info(f, app, content_area); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } ActiveRadarrBlock::UpdateAndScanPrompt => { let prompt = format!( @@ -73,7 +75,10 @@ impl DrawUi for MovieDetailsUi { .prompt(&prompt) .yes_no_value(app.data.radarr_data.prompt_confirm); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } ActiveRadarrBlock::ManualSearchConfirmPrompt => { draw_manual_search_confirm_prompt(f, app); @@ -82,14 +87,7 @@ impl DrawUi for MovieDetailsUi { } }; - draw_popup_over( - f, - app, - area, - draw_library, - draw_movie_info_popup, - Size::Large, - ); + draw_popup(f, app, draw_movie_info_popup, Size::Large); } } } @@ -371,7 +369,7 @@ fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { let (current_selection, is_empty) = match app.data.radarr_data.movie_details_modal.as_ref() { Some(movie_details_modal) if !movie_details_modal.movie_releases.items.is_empty() => ( movie_details_modal @@ -382,7 +380,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ), _ => (RadarrRelease::default(), true), }; - let current_route = *app.get_current_route(); + let current_route = app.get_current_route(); let mut default_movie_details_modal = MovieDetailsModal::default(); let help_footer = app .data @@ -532,7 +530,10 @@ fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) { .prompt(&prompt) .yes_no_value(app.data.radarr_data.prompt_confirm); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } } @@ -547,13 +548,3 @@ fn style_from_download_status(download_status: &str, is_monitored: bool, status: _ => Style::new().downloaded(), } } - -fn decorate_peer_style(seeders: u64, leechers: u64, text: Text<'_>) -> Text<'_> { - if seeders == 0 { - text.failure() - } else if seeders < leechers { - text.warning() - } else { - text.success() - } -} diff --git a/src/ui/radarr_ui/library/movie_details_ui_tests.rs b/src/ui/radarr_ui/library/movie_details_ui_tests.rs index 485594b..9d99d6f 100644 --- a/src/ui/radarr_ui/library/movie_details_ui_tests.rs +++ b/src/ui/radarr_ui/library/movie_details_ui_tests.rs @@ -2,13 +2,12 @@ mod tests { use pretty_assertions::assert_eq; use ratatui::style::Style; - use ratatui::text::Text; use rstest::rstest; use strum::IntoEnumIterator; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS}; use crate::ui::radarr_ui::library::movie_details_ui::{ - decorate_peer_style, style_from_download_status, MovieDetailsUi, + style_from_download_status, MovieDetailsUi, }; use crate::ui::styles::ManagarrStyle; use crate::ui::DrawUi; @@ -43,36 +42,4 @@ mod tests { expected_style ); } - - #[rstest] - #[case(0, 0, PeerStyle::Failure)] - #[case(1, 2, PeerStyle::Warning)] - #[case(4, 2, PeerStyle::Success)] - fn test_decorate_peer_style( - #[case] seeders: u64, - #[case] leechers: u64, - #[case] expected_style: PeerStyle, - ) { - let text = Text::from("test"); - match expected_style { - PeerStyle::Failure => assert_eq!( - decorate_peer_style(seeders, leechers, text.clone()), - text.failure() - ), - PeerStyle::Warning => assert_eq!( - decorate_peer_style(seeders, leechers, text.clone()), - text.warning() - ), - PeerStyle::Success => assert_eq!( - decorate_peer_style(seeders, leechers, text.clone()), - text.success() - ), - } - } - - enum PeerStyle { - Failure, - Warning, - Success, - } } diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index d19fabc..18a9f11 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -1,4 +1,4 @@ -use std::iter; +use std::{cmp, iter}; use chrono::{Duration, Utc}; use ratatui::layout::{Constraint, Layout, Rect}; @@ -34,14 +34,9 @@ mod collections; mod downloads; mod indexers; mod library; -mod radarr_ui_utils; mod root_folders; mod system; -#[cfg(test)] -#[path = "radarr_ui_tests.rs"] -mod radarr_ui_tests; - pub(super) struct RadarrUi; impl DrawUi for RadarrUi { @@ -51,7 +46,7 @@ impl DrawUi for RadarrUi { fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let content_area = draw_tabs(f, area, "Movies", &app.data.radarr_data.main_tabs); - let route = *app.get_current_route(); + let route = app.get_current_route(); match route { _ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area), @@ -178,15 +173,17 @@ fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { if !downloads_vec.is_empty() { f.render_widget(block, area); + let max_items = ((((area.height as f32 / 2.0).floor() * 2.0) as i32) / 2) - 1; + let items = cmp::min(downloads_vec.len(), max_items.unsigned_abs() as usize); let download_item_areas = Layout::vertical( iter::repeat(Constraint::Length(2)) - .take(downloads_vec.len()) + .take(items) .collect::>(), ) .margin(1) .split(area); - for i in 0..downloads_vec.len() { + for i in 0..items { let DownloadRecord { title, sizeleft, diff --git a/src/ui/radarr_ui/radarr_ui_utils.rs b/src/ui/radarr_ui/radarr_ui_utils.rs deleted file mode 100644 index 9cbadeb..0000000 --- a/src/ui/radarr_ui/radarr_ui_utils.rs +++ /dev/null @@ -1,46 +0,0 @@ -use ratatui::style::{Style, Stylize}; -use ratatui::widgets::ListItem; - -use crate::ui::styles::ManagarrStyle; - -#[cfg(test)] -#[path = "radarr_ui_utils_tests.rs"] -mod radarr_ui_utils_tests; - -pub(super) fn style_log_list_item(list_item: ListItem<'_>, level: String) -> ListItem<'_> { - match level.to_lowercase().as_str() { - "trace" => list_item.gray(), - "debug" => list_item.blue(), - "info" => list_item.style(Style::new().default()), - "warn" => list_item.style(Style::new().secondary()), - "error" => list_item.style(Style::new().failure()), - "fatal" => list_item.style(Style::new().failure().bold()), - _ => list_item.style(Style::new().default()), - } -} - -pub(super) fn convert_to_minutes_hours_days(time: i64) -> String { - if time < 60 { - if time == 0 { - "now".to_owned() - } else if time == 1 { - format!("{time} minute") - } else { - format!("{time} minutes") - } - } else if time / 60 < 24 { - let hours = time / 60; - if hours == 1 { - format!("{hours} hour") - } else { - format!("{hours} hours") - } - } else { - let days = time / (60 * 24); - if days == 1 { - format!("{days} day") - } else { - format!("{days} days") - } - } -} diff --git a/src/ui/radarr_ui/radarr_ui_utils_tests.rs b/src/ui/radarr_ui/radarr_ui_utils_tests.rs deleted file mode 100644 index ddcecf8..0000000 --- a/src/ui/radarr_ui/radarr_ui_utils_tests.rs +++ /dev/null @@ -1,70 +0,0 @@ -#[cfg(test)] -mod tests { - use super::super::*; - use pretty_assertions::assert_str_eq; - use ratatui::prelude::Text; - use ratatui::text::Span; - - #[test] - fn test_determine_log_style_by_level() { - let list_item = ListItem::new(Text::from(Span::raw("test"))); - - assert_eq!( - style_log_list_item(list_item.clone(), "trace".to_string()), - list_item.clone().gray() - ); - assert_eq!( - style_log_list_item(list_item.clone(), "debug".to_string()), - list_item.clone().blue() - ); - assert_eq!( - style_log_list_item(list_item.clone(), "info".to_string()), - list_item.clone().style(Style::new().default()) - ); - assert_eq!( - style_log_list_item(list_item.clone(), "warn".to_string()), - list_item.clone().style(Style::new().secondary()) - ); - assert_eq!( - style_log_list_item(list_item.clone(), "error".to_string()), - list_item.clone().style(Style::new().failure()) - ); - assert_eq!( - style_log_list_item(list_item.clone(), "fatal".to_string()), - list_item.clone().style(Style::new().failure().bold()) - ); - assert_eq!( - style_log_list_item(list_item.clone(), "".to_string()), - list_item.style(Style::new().default()) - ); - } - - #[test] - fn test_determine_log_style_by_level_case_insensitive() { - let list_item = ListItem::new(Text::from(Span::raw("test"))); - - assert_eq!( - style_log_list_item(list_item.clone(), "TrAcE".to_string()), - list_item.gray() - ); - } - - #[test] - fn test_convert_to_minutes_hours_days_minutes() { - assert_str_eq!(convert_to_minutes_hours_days(0), "now"); - assert_str_eq!(convert_to_minutes_hours_days(1), "1 minute"); - assert_str_eq!(convert_to_minutes_hours_days(2), "2 minutes"); - } - - #[test] - fn test_convert_to_minutes_hours_days_hours() { - assert_str_eq!(convert_to_minutes_hours_days(60), "1 hour"); - assert_str_eq!(convert_to_minutes_hours_days(120), "2 hours"); - } - - #[test] - fn test_convert_to_minutes_hours_days_days() { - assert_str_eq!(convert_to_minutes_hours_days(1440), "1 day"); - assert_str_eq!(convert_to_minutes_hours_days(2880), "2 days"); - } -} diff --git a/src/ui/radarr_ui/root_folders/mod.rs b/src/ui/radarr_ui/root_folders/mod.rs index 381b37c..52655ee 100644 --- a/src/ui/radarr_ui/root_folders/mod.rs +++ b/src/ui/radarr_ui/root_folders/mod.rs @@ -11,7 +11,7 @@ use crate::ui::utils::layout_block_top_border; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::{Popup, Size}; -use crate::ui::{draw_input_box_popup, draw_popup_over, DrawUi}; +use crate::ui::{draw_input_box_popup, draw_popup, DrawUi}; use crate::utils::convert_to_gb; #[cfg(test)] @@ -30,17 +30,13 @@ impl DrawUi for RootFoldersUi { } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { + draw_root_folders(f, app, area); + match active_radarr_block { - ActiveRadarrBlock::RootFolders => draw_root_folders(f, app, area), - ActiveRadarrBlock::AddRootFolderPrompt => draw_popup_over( - f, - app, - area, - draw_root_folders, - draw_add_root_folder_prompt_box, - Size::InputBox, - ), + ActiveRadarrBlock::AddRootFolderPrompt => { + draw_popup(f, app, draw_add_root_folder_prompt_box, Size::InputBox) + } ActiveRadarrBlock::DeleteRootFolderPrompt => { let prompt = format!( "Do you really want to delete this root folder: \n{}?", @@ -51,8 +47,10 @@ impl DrawUi for RootFoldersUi { .prompt(&prompt) .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_root_folders(f, app, area); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } _ => (), } diff --git a/src/ui/radarr_ui/system/mod.rs b/src/ui/radarr_ui/system/mod.rs index c46653e..6d90f65 100644 --- a/src/ui/radarr_ui/system/mod.rs +++ b/src/ui/radarr_ui/system/mod.rs @@ -15,10 +15,11 @@ use crate::app::App; use crate::models::radarr_models::RadarrTask; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::QueueEvent; -use crate::ui::radarr_ui::radarr_ui_utils::{convert_to_minutes_hours_days, style_log_list_item}; use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi; use crate::ui::styles::ManagarrStyle; -use crate::ui::utils::layout_block_top_border; +use crate::ui::utils::{ + convert_to_minutes_hours_days, layout_block_top_border, style_log_list_item, +}; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::selectable_list::SelectableList; @@ -61,19 +62,16 @@ impl DrawUi for SystemUi { } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let route = *app.get_current_route(); + let route = app.get_current_route(); + draw_system_ui_layout(f, app, area); - match route { - _ if SystemDetailsUi::accepts(route) => SystemDetailsUi::draw(f, app, area), - _ if matches!(route, Route::Radarr(ActiveRadarrBlock::System, _)) => { - draw_system_ui_layout(f, app, area) - } - _ => (), + if SystemDetailsUi::accepts(route) { + SystemDetailsUi::draw(f, app, area); } } } -pub(super) fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { +fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let [activities_area, logs_area, help_area] = Layout::vertical([ Constraint::Ratio(1, 2), Constraint::Ratio(1, 2), diff --git a/src/ui/radarr_ui/system/system_details_ui.rs b/src/ui/radarr_ui/system/system_details_ui.rs index cfc3c5c..4245d20 100644 --- a/src/ui/radarr_ui/system/system_details_ui.rs +++ b/src/ui/radarr_ui/system/system_details_ui.rs @@ -9,19 +9,17 @@ use crate::app::App; use crate::models::radarr_models::RadarrTask; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::Route; -use crate::ui::radarr_ui::radarr_ui_utils::style_log_list_item; use crate::ui::radarr_ui::system::{ - draw_queued_events, draw_system_ui_layout, extract_task_props, TASK_TABLE_CONSTRAINTS, - TASK_TABLE_HEADERS, + draw_queued_events, extract_task_props, TASK_TABLE_CONSTRAINTS, TASK_TABLE_HEADERS, }; use crate::ui::styles::ManagarrStyle; -use crate::ui::utils::{borderless_block, title_block}; +use crate::ui::utils::{borderless_block, style_log_list_item, title_block}; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::selectable_list::SelectableList; -use crate::ui::{draw_popup_over, DrawUi}; +use crate::ui::{draw_popup, DrawUi}; #[cfg(test)] #[path = "system_details_ui_tests.rs"] @@ -38,33 +36,19 @@ impl DrawUi for SystemDetailsUi { false } - fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { match active_radarr_block { ActiveRadarrBlock::SystemLogs => { - draw_system_ui_layout(f, app, area); draw_logs_popup(f, app); } ActiveRadarrBlock::SystemTasks | ActiveRadarrBlock::SystemTaskStartConfirmPrompt => { - draw_popup_over( - f, - app, - area, - draw_system_ui_layout, - draw_tasks_popup, - Size::Large, - ) + draw_popup(f, app, draw_tasks_popup, Size::Large) + } + ActiveRadarrBlock::SystemQueuedEvents => { + draw_popup(f, app, draw_queued_events, Size::Medium) } - ActiveRadarrBlock::SystemQueuedEvents => draw_popup_over( - f, - app, - area, - draw_system_ui_layout, - draw_queued_events, - Size::Medium, - ), ActiveRadarrBlock::SystemUpdates => { - draw_system_ui_layout(f, app, area); draw_updates_popup(f, app); } _ => (), @@ -145,7 +129,10 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .prompt(&prompt) .yes_no_value(app.data.radarr_data.prompt_confirm); - f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.area()); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); } } diff --git a/src/ui/sonarr_ui/blocklist/blocklist_ui_tests.rs b/src/ui/sonarr_ui/blocklist/blocklist_ui_tests.rs new file mode 100644 index 0000000..009adc0 --- /dev/null +++ b/src/ui/sonarr_ui/blocklist/blocklist_ui_tests.rs @@ -0,0 +1,18 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS}; + use crate::ui::sonarr_ui::blocklist::BlocklistUi; + use crate::ui::DrawUi; + use strum::IntoEnumIterator; + + #[test] + fn test_blocklist_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if BLOCKLIST_BLOCKS.contains(&active_sonarr_block) { + assert!(BlocklistUi::accepts(active_sonarr_block.into())); + } else { + assert!(!BlocklistUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/blocklist/mod.rs b/src/ui/sonarr_ui/blocklist/mod.rs new file mode 100644 index 0000000..442c98a --- /dev/null +++ b/src/ui/sonarr_ui/blocklist/mod.rs @@ -0,0 +1,177 @@ +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKLIST_BLOCKS}; +use crate::models::sonarr_models::BlocklistItem; +use crate::models::Route; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::{Line, Text}; +use ratatui::widgets::{Cell, Row}; +use ratatui::Frame; + +#[cfg(test)] +#[path = "blocklist_ui_tests.rs"] +mod blocklist_ui_tests; + +pub(super) struct BlocklistUi; + +impl DrawUi for BlocklistUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return BLOCKLIST_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + draw_blocklist_table(f, app, area); + + match active_sonarr_block { + ActiveSonarrBlock::BlocklistItemDetails => { + draw_blocklist_item_details_popup(f, app); + } + ActiveSonarrBlock::DeleteBlocklistItemPrompt => { + let prompt = format!( + "Do you want to remove this item from your blocklist: \n{}?", + app + .data + .sonarr_data + .blocklist + .current_selection() + .source_title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Remove Item from Blocklist") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveSonarrBlock::BlocklistClearAllItemsPrompt => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Clear Blocklist") + .prompt("Do you want to clear your blocklist?") + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::SmallPrompt), + f.area(), + ); + } + _ => (), + } + } + } +} + +fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let blocklist_table_footer = app + .data + .sonarr_data + .main_tabs + .get_active_tab_contextual_help(); + + let blocklist_row_mapping = |blocklist_item: &BlocklistItem| { + let BlocklistItem { + source_title, + series_title, + languages, + quality, + date, + .. + } = blocklist_item; + + let title = series_title.as_ref().unwrap_or(&String::new()).to_owned(); + let languages_string = languages + .iter() + .map(|lang| lang.name.to_owned()) + .collect::>() + .join(", "); + + Row::new(vec![ + Cell::from(title), + Cell::from(source_title.to_owned()), + Cell::from(languages_string), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let blocklist_table = ManagarrTable::new( + Some(&mut app.data.sonarr_data.blocklist), + blocklist_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(blocklist_table_footer) + .sorting(active_sonarr_block == ActiveSonarrBlock::BlocklistSortPrompt) + .headers([ + "Series Title", + "Source Title", + "Language", + "Quality", + "Date", + ]) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(40), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(15), + ]); + + f.render_widget(blocklist_table, area); + } +} + +fn draw_blocklist_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = if app.data.sonarr_data.blocklist.items.is_empty() { + BlocklistItem::default() + } else { + app.data.sonarr_data.blocklist.current_selection().clone() + }; + let BlocklistItem { + source_title, + protocol, + indexer, + message, + .. + } = current_selection; + let text = Text::from(vec![ + Line::from(vec![ + "Name: ".bold().secondary(), + source_title.to_owned().secondary(), + ]), + Line::from(vec![ + "Protocol: ".bold().secondary(), + protocol.to_owned().secondary(), + ]), + Line::from(vec![ + "Indexer: ".bold().secondary(), + indexer.to_owned().secondary(), + ]), + Line::from(vec![ + "Message: ".bold().secondary(), + message.to_owned().secondary(), + ]), + ]); + + let message = Message::new(text) + .title("Details") + .style(Style::new().secondary()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area()); +} diff --git a/src/ui/sonarr_ui/downloads/downloads_ui_tests.rs b/src/ui/sonarr_ui/downloads/downloads_ui_tests.rs new file mode 100644 index 0000000..2d040ae --- /dev/null +++ b/src/ui/sonarr_ui/downloads/downloads_ui_tests.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS}; + use crate::ui::sonarr_ui::downloads::DownloadsUi; + use crate::ui::DrawUi; + + #[test] + fn test_downloads_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if DOWNLOADS_BLOCKS.contains(&active_sonarr_block) { + assert!(DownloadsUi::accepts(active_sonarr_block.into())); + } else { + assert!(!DownloadsUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/downloads/mod.rs b/src/ui/sonarr_ui/downloads/mod.rs new file mode 100644 index 0000000..ba3238f --- /dev/null +++ b/src/ui/sonarr_ui/downloads/mod.rs @@ -0,0 +1,152 @@ +use ratatui::layout::{Constraint, Rect}; +use ratatui::widgets::{Cell, Row}; +use ratatui::Frame; + +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DOWNLOADS_BLOCKS}; +use crate::models::sonarr_models::DownloadRecord; +use crate::models::{HorizontallyScrollableText, Route}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; +use crate::utils::convert_f64_to_gb; + +#[cfg(test)] +#[path = "downloads_ui_tests.rs"] +mod downloads_ui_tests; + +pub(super) struct DownloadsUi; + +impl DrawUi for DownloadsUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return DOWNLOADS_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + draw_downloads(f, app, area); + + match active_sonarr_block { + ActiveSonarrBlock::DeleteDownloadPrompt => { + let prompt = format!( + "Do you really want to delete this download: \n{}?", + app.data.sonarr_data.downloads.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Cancel Download") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveSonarrBlock::UpdateDownloadsPrompt => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update Downloads") + .prompt("Do you want to update your downloads?") + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + } + } + } +} + +fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = if app.data.sonarr_data.downloads.items.is_empty() { + DownloadRecord::default() + } else { + app.data.sonarr_data.downloads.current_selection().clone() + }; + let downloads_table_footer = app + .data + .sonarr_data + .main_tabs + .get_active_tab_contextual_help(); + + let downloads_row_mapping = |download_record: &DownloadRecord| { + let DownloadRecord { + title, + size, + sizeleft, + download_client, + indexer, + output_path, + .. + } = download_record; + + if output_path.is_some() { + output_path.as_ref().unwrap().scroll_left_or_reset( + get_width_from_percentage(area, 18), + current_selection == *download_record, + app.tick_count % app.ticks_until_scroll == 0, + ); + } + + let percent = if *size == 0.0 { + 0.0 + } else { + 1f64 - (*sizeleft / *size) + }; + let file_size: f64 = convert_f64_to_gb(*size); + + Row::new(vec![ + Cell::from(title.to_owned()), + Cell::from(format!("{:.0}%", percent * 100.0)), + Cell::from(format!("{file_size:.2} GB")), + Cell::from( + output_path + .as_ref() + .unwrap_or(&HorizontallyScrollableText::default()) + .to_string(), + ), + Cell::from(indexer.to_owned()), + Cell::from( + download_client + .as_ref() + .unwrap_or(&String::new()) + .to_owned(), + ), + ]) + .primary() + }; + let downloads_table = ManagarrTable::new( + Some(&mut app.data.sonarr_data.downloads), + downloads_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(downloads_table_footer) + .headers([ + "Title", + "Percent Complete", + "Size", + "Output Path", + "Indexer", + "Download Client", + ]) + .constraints([ + Constraint::Percentage(30), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(18), + Constraint::Percentage(17), + Constraint::Percentage(13), + ]); + + f.render_widget(downloads_table, area); +} diff --git a/src/ui/sonarr_ui/history/history_ui_tests.rs b/src/ui/sonarr_ui/history/history_ui_tests.rs new file mode 100644 index 0000000..bae662e --- /dev/null +++ b/src/ui/sonarr_ui/history/history_ui_tests.rs @@ -0,0 +1,18 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; + use crate::ui::sonarr_ui::history::HistoryUi; + use crate::ui::DrawUi; + use strum::IntoEnumIterator; + + #[test] + fn test_history_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if HISTORY_BLOCKS.contains(&active_sonarr_block) { + assert!(HistoryUi::accepts(active_sonarr_block.into())); + } else { + assert!(!HistoryUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/history/mod.rs b/src/ui/sonarr_ui/history/mod.rs new file mode 100644 index 0000000..268bfdc --- /dev/null +++ b/src/ui/sonarr_ui/history/mod.rs @@ -0,0 +1,158 @@ +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; +use crate::models::sonarr_models::{SonarrHistoryEventType, SonarrHistoryItem}; +use crate::models::Route; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::style::Style; +use ratatui::text::Text; +use ratatui::widgets::{Cell, Row}; +use ratatui::Frame; + +use super::sonarr_ui_utils::{ + create_download_failed_history_event_details, + create_download_folder_imported_history_event_details, + create_episode_file_deleted_history_event_details, + create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, + create_no_data_history_event_details, +}; + +#[cfg(test)] +#[path = "history_ui_tests.rs"] +mod history_ui_tests; + +pub(super) struct HistoryUi; + +impl DrawUi for HistoryUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return HISTORY_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + draw_history_table(f, app, area); + + if active_sonarr_block == ActiveSonarrBlock::HistoryItemDetails { + draw_history_item_details_popup(f, app); + } + } + } +} + +fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = if app.data.sonarr_data.history.items.is_empty() { + SonarrHistoryItem::default() + } else { + app.data.sonarr_data.history.current_selection().clone() + }; + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let history_table_footer = app + .data + .sonarr_data + .main_tabs + .get_active_tab_contextual_help(); + + let history_row_mapping = |history_item: &SonarrHistoryItem| { + let SonarrHistoryItem { + source_title, + languages, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 40), + current_selection == *history_item, + app.tick_count % app.ticks_until_scroll == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from( + languages + .iter() + .map(|language| language.name.to_owned()) + .collect::>() + .join(","), + ), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let history_table = + ManagarrTable::new(Some(&mut app.data.sonarr_data.history), history_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(history_table_footer) + .sorting(active_sonarr_block == ActiveSonarrBlock::HistorySortPrompt) + .searching(active_sonarr_block == ActiveSonarrBlock::SearchHistory) + .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchHistoryError) + .filtering(active_sonarr_block == ActiveSonarrBlock::FilterHistory) + .filter_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::FilterHistoryError) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); + + if [ + ActiveSonarrBlock::SearchHistory, + ActiveSonarrBlock::FilterHistory, + ] + .contains(&active_sonarr_block) + { + history_table.show_cursor(f, area); + } + + f.render_widget(history_table, area); + } +} + +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = if app.data.sonarr_data.history.items.is_empty() { + SonarrHistoryItem::default() + } else { + app.data.sonarr_data.history.current_selection().clone() + }; + + let line_vec = match current_selection.event_type { + SonarrHistoryEventType::Grabbed => create_grabbed_history_event_details(current_selection), + SonarrHistoryEventType::DownloadFolderImported => { + create_download_folder_imported_history_event_details(current_selection) + } + SonarrHistoryEventType::DownloadFailed => { + create_download_failed_history_event_details(current_selection) + } + SonarrHistoryEventType::EpisodeFileDeleted => { + create_episode_file_deleted_history_event_details(current_selection) + } + SonarrHistoryEventType::EpisodeFileRenamed => { + create_episode_file_renamed_history_event_details(current_selection) + } + _ => create_no_data_history_event_details(current_selection), + }; + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(Style::new().secondary()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area()); +} diff --git a/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs b/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs new file mode 100644 index 0000000..2042890 --- /dev/null +++ b/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs @@ -0,0 +1,177 @@ +use std::sync::atomic::Ordering; + +use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; +use crate::models::Route; +use crate::render_selectable_input_box; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::title_block_centered; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::popup::Size; +use crate::ui::{draw_popup, DrawUi}; +use ratatui::layout::{Constraint, Flex, Layout, Rect}; +use ratatui::text::Text; +use ratatui::widgets::Paragraph; +use ratatui::Frame; + +#[cfg(test)] +#[path = "edit_indexer_ui_tests.rs"] +mod edit_indexer_ui_tests; + +pub(super) struct EditIndexerUi; + +impl DrawUi for EditIndexerUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return EDIT_INDEXER_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_edit_indexer_prompt, Size::WideLargePrompt); + } +} + +fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let block = title_block_centered("Edit Indexer"); + let yes_no_value = app.data.sonarr_data.prompt_confirm; + let selected_block = app.data.sonarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveSonarrBlock::EditIndexerConfirmPrompt; + let edit_indexer_modal_option = &app.data.sonarr_data.edit_indexer_modal; + let protocol = &app.data.sonarr_data.indexers.current_selection().protocol; + let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); + let help_paragraph = Paragraph::new(help_text).centered(); + + if edit_indexer_modal_option.is_some() { + let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap(); + + let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(18), + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(1), + ]) + .margin(1) + .areas(area); + let [left_side_area, right_side_area] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) + .margin(1) + .areas(settings_area); + let [name_area, rss_area, auto_search_area, interactive_search_area, priority_area] = + Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ]) + .areas(left_side_area); + let [url_area, api_key_area, seed_ratio_area, tags_area] = Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ]) + .areas(right_side_area); + + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let priority = edit_indexer_modal.priority.to_string(); + let name_input_box = InputBox::new(&edit_indexer_modal.name.text) + .offset(edit_indexer_modal.name.offset.load(Ordering::SeqCst)) + .label("Name") + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerNameInput) + .selected(active_sonarr_block == ActiveSonarrBlock::EditIndexerNameInput); + let url_input_box = InputBox::new(&edit_indexer_modal.url.text) + .offset(edit_indexer_modal.url.offset.load(Ordering::SeqCst)) + .label("URL") + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerUrlInput) + .selected(active_sonarr_block == ActiveSonarrBlock::EditIndexerUrlInput); + let api_key_input_box = InputBox::new(&edit_indexer_modal.api_key.text) + .offset(edit_indexer_modal.api_key.offset.load(Ordering::SeqCst)) + .label("API Key") + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerApiKeyInput) + .selected(active_sonarr_block == ActiveSonarrBlock::EditIndexerApiKeyInput); + let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) + .offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerTagsInput) + .selected(active_sonarr_block == ActiveSonarrBlock::EditIndexerTagsInput); + let priority_input_box = InputBox::new(&priority) + .cursor_after_string(false) + .label("Indexer Priority ▴▾") + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerPriorityInput) + .selected(active_sonarr_block == ActiveSonarrBlock::EditIndexerPriorityInput); + + render_selectable_input_box!(name_input_box, f, name_area); + render_selectable_input_box!(url_input_box, f, url_area); + render_selectable_input_box!(api_key_input_box, f, api_key_area); + + if protocol == "torrent" { + let seed_ratio_input_box = InputBox::new(&edit_indexer_modal.seed_ratio.text) + .offset(edit_indexer_modal.seed_ratio.offset.load(Ordering::SeqCst)) + .label("Seed Ratio") + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerSeedRatioInput) + .selected(active_sonarr_block == ActiveSonarrBlock::EditIndexerSeedRatioInput); + let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) + .offset(edit_indexer_modal.tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerTagsInput) + .selected(active_sonarr_block == ActiveSonarrBlock::EditIndexerTagsInput); + + render_selectable_input_box!(seed_ratio_input_box, f, seed_ratio_area); + render_selectable_input_box!(tags_input_box, f, tags_area); + render_selectable_input_box!(priority_input_box, f, priority_area); + } else { + render_selectable_input_box!(tags_input_box, f, seed_ratio_area); + render_selectable_input_box!(priority_input_box, f, tags_area); + } + + let rss_checkbox = Checkbox::new("Enable RSS") + .checked(edit_indexer_modal.enable_rss.unwrap_or_default()) + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerToggleEnableRss); + let auto_search_checkbox = Checkbox::new("Enable Automatic Search") + .checked( + edit_indexer_modal + .enable_automatic_search + .unwrap_or_default(), + ) + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch); + let interactive_search_checkbox = Checkbox::new("Enable Interactive Search") + .checked( + edit_indexer_modal + .enable_interactive_search + .unwrap_or_default(), + ) + .highlighted(selected_block == ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch); + + let [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) + .flex(Flex::Center) + .areas(buttons_area); + + let save_button = Button::new() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(block, area); + f.render_widget(rss_checkbox, rss_area); + f.render_widget(auto_search_checkbox, auto_search_area); + f.render_widget(interactive_search_checkbox, interactive_search_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); + f.render_widget(help_paragraph, help_area); + } + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} diff --git a/src/ui/sonarr_ui/indexers/edit_indexer_ui_tests.rs b/src/ui/sonarr_ui/indexers/edit_indexer_ui_tests.rs new file mode 100644 index 0000000..362bc61 --- /dev/null +++ b/src/ui/sonarr_ui/indexers/edit_indexer_ui_tests.rs @@ -0,0 +1,18 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; + use crate::ui::sonarr_ui::indexers::edit_indexer_ui::EditIndexerUi; + use crate::ui::DrawUi; + use strum::IntoEnumIterator; + + #[test] + fn test_edit_indexer_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if EDIT_INDEXER_BLOCKS.contains(&active_sonarr_block) { + assert!(EditIndexerUi::accepts(active_sonarr_block.into())); + } else { + assert!(!EditIndexerUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs b/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs new file mode 100644 index 0000000..4d284aa --- /dev/null +++ b/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs @@ -0,0 +1,119 @@ +use ratatui::layout::{Constraint, Flex, Layout, Rect}; +use ratatui::text::Text; +use ratatui::widgets::Paragraph; +use ratatui::Frame; + +use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, +}; +use crate::models::Route; +use crate::render_selectable_input_box; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::title_block_centered; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::popup::Size; +use crate::ui::{draw_popup, DrawUi}; + +#[cfg(test)] +#[path = "indexer_settings_ui_tests.rs"] +mod indexer_settings_ui_tests; + +pub(super) struct IndexerSettingsUi; + +impl DrawUi for IndexerSettingsUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return INDEXER_SETTINGS_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_edit_indexer_settings_prompt, Size::LargePrompt); + } +} + +fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let block = title_block_centered("Configure All Indexer Settings"); + let yes_no_value = app.data.sonarr_data.prompt_confirm; + let selected_block = app.data.sonarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveSonarrBlock::IndexerSettingsConfirmPrompt; + let indexer_settings_option = &app.data.sonarr_data.indexer_settings; + let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); + let help_paragraph = Paragraph::new(help_text).centered(); + + if indexer_settings_option.is_some() { + let indexer_settings = indexer_settings_option.as_ref().unwrap(); + + let [_, min_age_area, retention_area, max_size_area, rss_sync_area, _, buttons_area, help_area] = + Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(1), + ]) + .margin(1) + .areas(area); + + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let min_age = indexer_settings.minimum_age.to_string(); + let retention = indexer_settings.retention.to_string(); + let max_size = indexer_settings.maximum_size.to_string(); + let rss_sync_interval = indexer_settings.rss_sync_interval.to_string(); + + let min_age_text_box = InputBox::new(&min_age) + .cursor_after_string(false) + .label("Minimum Age (minutes) ▴▾") + .highlighted(selected_block == ActiveSonarrBlock::IndexerSettingsMinimumAgeInput) + .selected(active_sonarr_block == ActiveSonarrBlock::IndexerSettingsMinimumAgeInput); + let retention_input_box = InputBox::new(&retention) + .cursor_after_string(false) + .label("Retention (days) ▴▾") + .highlighted(selected_block == ActiveSonarrBlock::IndexerSettingsRetentionInput) + .selected(active_sonarr_block == ActiveSonarrBlock::IndexerSettingsRetentionInput); + let max_size_input_box = InputBox::new(&max_size) + .cursor_after_string(false) + .label("Maximum Size (MB) ▴▾") + .highlighted(selected_block == ActiveSonarrBlock::IndexerSettingsMaximumSizeInput) + .selected(active_sonarr_block == ActiveSonarrBlock::IndexerSettingsMaximumSizeInput); + let rss_sync_interval_input_box = InputBox::new(&rss_sync_interval) + .cursor_after_string(false) + .label("RSS Sync Interval (minutes) ▴▾") + .highlighted(selected_block == ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput) + .selected(active_sonarr_block == ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput); + + render_selectable_input_box!(min_age_text_box, f, min_age_area); + render_selectable_input_box!(retention_input_box, f, retention_area); + render_selectable_input_box!(max_size_input_box, f, max_size_area); + render_selectable_input_box!(rss_sync_interval_input_box, f, rss_sync_area); + } + + let [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) + .flex(Flex::Center) + .areas(buttons_area); + + let save_button = Button::new() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(block, area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); + f.render_widget(help_paragraph, help_area); + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} diff --git a/src/ui/sonarr_ui/indexers/indexer_settings_ui_tests.rs b/src/ui/sonarr_ui/indexers/indexer_settings_ui_tests.rs new file mode 100644 index 0000000..f95304f --- /dev/null +++ b/src/ui/sonarr_ui/indexers/indexer_settings_ui_tests.rs @@ -0,0 +1,21 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, + }; + use crate::ui::sonarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; + use crate::ui::DrawUi; + + #[test] + fn test_indexer_settings_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if INDEXER_SETTINGS_BLOCKS.contains(&active_sonarr_block) { + assert!(IndexerSettingsUi::accepts(active_sonarr_block.into())); + } else { + assert!(!IndexerSettingsUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/indexers/indexers_ui_tests.rs b/src/ui/sonarr_ui/indexers/indexers_ui_tests.rs new file mode 100644 index 0000000..84a548d --- /dev/null +++ b/src/ui/sonarr_ui/indexers/indexers_ui_tests.rs @@ -0,0 +1,27 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, + }; + use crate::ui::sonarr_ui::indexers::IndexersUi; + use crate::ui::DrawUi; + + #[test] + fn test_indexers_ui_accepts() { + let mut indexers_blocks = Vec::new(); + indexers_blocks.extend(INDEXERS_BLOCKS); + indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS); + indexers_blocks.extend(EDIT_INDEXER_BLOCKS); + indexers_blocks.push(ActiveSonarrBlock::TestAllIndexers); + + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if indexers_blocks.contains(&active_sonarr_block) { + assert!(IndexersUi::accepts(active_sonarr_block.into())); + } else { + assert!(!IndexersUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/indexers/mod.rs b/src/ui/sonarr_ui/indexers/mod.rs new file mode 100644 index 0000000..a0f5d67 --- /dev/null +++ b/src/ui/sonarr_ui/indexers/mod.rs @@ -0,0 +1,189 @@ +use ratatui::layout::{Constraint, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::Text; +use ratatui::widgets::{Cell, Row}; +use ratatui::Frame; + +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, INDEXERS_BLOCKS}; +use crate::models::servarr_models::Indexer; +use crate::models::Route; +use crate::ui::sonarr_ui::indexers::edit_indexer_ui::EditIndexerUi; +use crate::ui::sonarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; +use crate::ui::sonarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{layout_block_top_border, title_block}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; + +mod edit_indexer_ui; +mod indexer_settings_ui; +mod test_all_indexers_ui; + +#[cfg(test)] +#[path = "indexers_ui_tests.rs"] +mod indexers_ui_tests; + +pub(super) struct IndexersUi; + +impl DrawUi for IndexersUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return EditIndexerUi::accepts(route) + || IndexerSettingsUi::accepts(route) + || TestAllIndexersUi::accepts(route) + || INDEXERS_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + draw_indexers(f, app, area); + + match route { + _ if EditIndexerUi::accepts(route) => EditIndexerUi::draw(f, app, area), + _ if IndexerSettingsUi::accepts(route) => IndexerSettingsUi::draw(f, app, area), + _ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, area), + Route::Sonarr(active_sonarr_block, _) => match active_sonarr_block { + ActiveSonarrBlock::TestIndexer => { + if app.is_loading || app.data.sonarr_data.indexer_test_errors.is_none() { + let loading_popup = Popup::new(LoadingBlock::new( + app.is_loading || app.data.sonarr_data.indexer_test_errors.is_none(), + title_block("Testing Indexer"), + )) + .size(Size::LargeMessage); + f.render_widget(loading_popup, f.area()); + } else { + let popup = { + let result = app + .data + .sonarr_data + .indexer_test_errors + .as_ref() + .expect("Test result is unpopulated"); + + if !result.is_empty() { + Popup::new(Message::new(result.clone())).size(Size::LargeMessage) + } else { + let message = Message::new("Indexer test succeeded!") + .title("Success") + .style(Style::new().success().bold()); + Popup::new(message).size(Size::Message) + } + }; + + f.render_widget(popup, f.area()); + } + } + ActiveSonarrBlock::DeleteIndexerPrompt => { + let prompt = format!( + "Do you really want to delete this indexer: \n{}?", + app + .data + .sonarr_data + .indexers + .current_selection() + .name + .clone() + .unwrap_or_default() + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Indexer") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + }, + _ => (), + } + } +} + +fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let indexers_row_mapping = |indexer: &'_ Indexer| { + let Indexer { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + priority, + tags, + .. + } = indexer; + let bool_to_text = |flag: bool| { + if flag { + return Text::from("Enabled").success(); + } + + Text::from("Disabled").failure() + }; + + let rss = bool_to_text(*enable_rss); + let automatic_search = bool_to_text(*enable_automatic_search); + let interactive_search = bool_to_text(*enable_interactive_search); + let tags: String = tags + .iter() + .map(|tag_id| { + app + .data + .sonarr_data + .tags_map + .get_by_left(&tag_id.as_i64().unwrap()) + .unwrap() + .clone() + }) + .collect::>() + .join(", "); + + Row::new(vec![ + Cell::from(name.clone().unwrap_or_default()), + Cell::from(rss), + Cell::from(automatic_search), + Cell::from(interactive_search), + Cell::from(priority.to_string()), + Cell::from(tags), + ]) + .primary() + }; + let indexers_table_footer = app + .data + .sonarr_data + .main_tabs + .get_active_tab_contextual_help(); + let indexers_table = ManagarrTable::new( + Some(&mut app.data.sonarr_data.indexers), + indexers_row_mapping, + ) + .block(layout_block_top_border()) + .footer(indexers_table_footer) + .loading(app.is_loading) + .headers([ + "Indexer", + "RSS", + "Automatic Search", + "Interactive Search", + "Priority", + "Tags", + ]) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(23), + ]); + + f.render_widget(indexers_table, area); +} diff --git a/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs new file mode 100644 index 0000000..7bc3739 --- /dev/null +++ b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs @@ -0,0 +1,85 @@ +use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES}; +use crate::app::App; +use crate::models::servarr_data::modals::IndexerTestResultModalItem; +use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; +use crate::models::Route; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{borderless_block, get_width_from_percentage, title_block}; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::Size; +use crate::ui::{draw_popup, DrawUi}; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::widgets::{Cell, Row}; +use ratatui::Frame; + +#[cfg(test)] +#[path = "test_all_indexers_ui_tests.rs"] +mod test_all_indexers_ui_tests; + +pub(super) struct TestAllIndexersUi; + +impl DrawUi for TestAllIndexersUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return active_sonarr_block == ActiveSonarrBlock::TestAllIndexers; + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + draw_popup(f, app, draw_test_all_indexers_test_results, Size::Large); + } +} + +fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let is_loading = app.is_loading || app.data.sonarr_data.indexer_test_all_results.is_none(); + let current_selection = + if let Some(test_all_results) = app.data.sonarr_data.indexer_test_all_results.as_ref() { + test_all_results.current_selection().clone() + } else { + IndexerTestResultModalItem::default() + }; + f.render_widget(title_block("Test All Indexers"), area); + let help_footer = format!( + "<↑↓> scroll | {}", + build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) + ); + let test_results_row_mapping = |result: &IndexerTestResultModalItem| { + result.validation_failures.scroll_left_or_reset( + get_width_from_percentage(area, 86), + *result == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); + let pass_fail = if result.is_valid { "✔" } else { "❌" }; + let row = Row::new(vec![ + Cell::from(result.name.to_owned()), + Cell::from(pass_fail.to_owned()), + Cell::from(result.validation_failures.to_string()), + ]); + + if result.is_valid { + row.success() + } else { + row.failure() + } + }; + + let indexers_test_results_table = ManagarrTable::new( + app.data.sonarr_data.indexer_test_all_results.as_mut(), + test_results_row_mapping, + ) + .block(borderless_block()) + .loading(is_loading) + .footer(Some(help_footer)) + .footer_alignment(Alignment::Center) + .margin(1) + .headers(["Indexer", "Pass/Fail", "Failure Messages"]) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(70), + ]); + + f.render_widget(indexers_test_results_table, area); +} diff --git a/src/ui/sonarr_ui/indexers/test_all_indexers_ui_tests.rs b/src/ui/sonarr_ui/indexers/test_all_indexers_ui_tests.rs new file mode 100644 index 0000000..16f7e2e --- /dev/null +++ b/src/ui/sonarr_ui/indexers/test_all_indexers_ui_tests.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; + use crate::ui::sonarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; + use crate::ui::DrawUi; + + #[test] + fn test_test_all_indexers_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if active_sonarr_block == ActiveSonarrBlock::TestAllIndexers { + assert!(TestAllIndexersUi::accepts(active_sonarr_block.into())); + } else { + assert!(!TestAllIndexersUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/library/add_series_ui.rs b/src/ui/sonarr_ui/library/add_series_ui.rs new file mode 100644 index 0000000..3d95fbd --- /dev/null +++ b/src/ui/sonarr_ui/library/add_series_ui.rs @@ -0,0 +1,461 @@ +use std::sync::atomic::Ordering; + +use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::text::Text; +use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; +use ratatui::Frame; + +use crate::app::context_clues::{ + build_context_clue_string, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, +}; +use crate::app::sonarr::sonarr_context_clues::ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES; +use crate::models::servarr_data::sonarr::modals::AddSeriesModal; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS}; +use crate::models::sonarr_models::AddSeriesSearchResult; +use crate::models::{EnumDisplayStyle, Route}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{ + borderless_block, get_width_from_percentage, layout_block, layout_paragraph_borderless, + title_block_centered, +}; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::ui::{draw_popup, DrawUi}; +use crate::{render_selectable_input_box, App}; + +#[cfg(test)] +#[path = "add_series_ui_tests.rs"] +mod add_series_ui_tests; + +pub(super) struct AddSeriesUi; + +impl DrawUi for AddSeriesUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return ADD_SERIES_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + draw_popup(f, app, draw_add_series_search, Size::Large); + + match active_sonarr_block { + ActiveSonarrBlock::AddSeriesPrompt + | ActiveSonarrBlock::AddSeriesSelectMonitor + | ActiveSonarrBlock::AddSeriesSelectSeriesType + | ActiveSonarrBlock::AddSeriesSelectQualityProfile + | ActiveSonarrBlock::AddSeriesSelectLanguageProfile + | ActiveSonarrBlock::AddSeriesSelectRootFolder + | ActiveSonarrBlock::AddSeriesTagsInput => { + draw_popup(f, app, draw_confirmation_popup, Size::Long); + } + ActiveSonarrBlock::AddSeriesAlreadyInLibrary => { + f.render_widget( + Popup::new(Message::new("This series is already in your library")).size(Size::Message), + f.area(), + ); + } + _ => (), + } + } + } +} + +fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let is_loading = app.is_loading || app.data.sonarr_data.add_searched_series.is_none(); + let current_selection = + if let Some(add_searched_series) = app.data.sonarr_data.add_searched_series.as_ref() { + add_searched_series.current_selection().clone() + } else { + AddSeriesSearchResult::default() + }; + + let [search_box_area, results_area, help_area] = Layout::vertical([ + Constraint::Length(3), + Constraint::Fill(0), + Constraint::Length(3), + ]) + .margin(1) + .areas(area); + let block_content = &app + .data + .sonarr_data + .add_series_search + .as_ref() + .unwrap() + .text; + let offset = app + .data + .sonarr_data + .add_series_search + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst); + let search_results_row_mapping = |series: &AddSeriesSearchResult| { + let rating = series.ratings.clone().unwrap_or_default().value; + let series_rating = if rating == 0.0 { + String::new() + } else { + format!("{rating:.1}") + }; + let in_library = if app + .data + .sonarr_data + .series + .items + .iter() + .any(|mov| mov.tvdb_id == series.tvdb_id) + { + "✔" + } else { + "" + }; + let network = series.network.clone().unwrap_or_default(); + let seasons = if let Some(ref stats) = series.statistics { + format!("{}", stats.season_count) + } else { + String::new() + }; + + series.title.scroll_left_or_reset( + get_width_from_percentage(area, 27), + *series == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); + + Row::new(vec![ + Cell::from(in_library), + Cell::from(series.title.to_string()), + Cell::from(series.year.to_string()), + Cell::from(network), + Cell::from(series_rating), + Cell::from(seasons), + Cell::from(series.genres.join(", ")), + ]) + .primary() + }; + + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + match active_sonarr_block { + ActiveSonarrBlock::AddSeriesSearchInput => { + let search_box = InputBox::new(block_content) + .offset(offset) + .block(title_block_centered("Add Series")); + let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); + let help_paragraph = Paragraph::new(help_text) + .block(borderless_block()) + .centered(); + + search_box.show_cursor(f, search_box_area); + f.render_widget(layout_block(), results_area); + f.render_widget(search_box, search_box_area); + f.render_widget(help_paragraph, help_area); + } + ActiveSonarrBlock::AddSeriesEmptySearchResults => { + let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); + let help_paragraph = Paragraph::new(help_text) + .block(borderless_block()) + .centered(); + let error_message = Message::new("No series found matching your query!"); + let error_message_popup = Popup::new(error_message).size(Size::Message); + + f.render_widget(layout_block(), results_area); + f.render_widget(error_message_popup, f.area()); + f.render_widget(help_paragraph, help_area); + } + ActiveSonarrBlock::AddSeriesSearchResults + | ActiveSonarrBlock::AddSeriesPrompt + | ActiveSonarrBlock::AddSeriesSelectMonitor + | ActiveSonarrBlock::AddSeriesSelectSeriesType + | ActiveSonarrBlock::AddSeriesSelectQualityProfile + | ActiveSonarrBlock::AddSeriesSelectLanguageProfile + | ActiveSonarrBlock::AddSeriesSelectRootFolder + | ActiveSonarrBlock::AddSeriesAlreadyInLibrary + | ActiveSonarrBlock::AddSeriesTagsInput => { + let help_text = + Text::from(build_context_clue_string(&ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES).help()); + let help_paragraph = Paragraph::new(help_text) + .block(borderless_block()) + .centered(); + let search_results_table = ManagarrTable::new( + app.data.sonarr_data.add_searched_series.as_mut(), + search_results_row_mapping, + ) + .loading(is_loading) + .block(layout_block()) + .headers([ + "✔", "Title", "Year", "Network", "Seasons", "Rating", "Genres", + ]) + .constraints([ + Constraint::Percentage(2), + Constraint::Percentage(27), + Constraint::Percentage(9), + Constraint::Percentage(13), + Constraint::Percentage(9), + Constraint::Percentage(9), + Constraint::Percentage(28), + ]); + + f.render_widget(search_results_table, results_area); + f.render_widget(help_paragraph, help_area); + } + _ => (), + } + } + + f.render_widget( + InputBox::new(block_content) + .offset(offset) + .block(title_block_centered("Add Series")), + search_box_area, + ); +} + +fn draw_confirmation_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + match active_sonarr_block { + ActiveSonarrBlock::AddSeriesSelectMonitor => { + draw_confirmation_prompt(f, app, area); + draw_add_series_select_monitor_popup(f, app); + } + ActiveSonarrBlock::AddSeriesSelectSeriesType => { + draw_confirmation_prompt(f, app, area); + draw_add_series_select_series_type_popup(f, app); + } + ActiveSonarrBlock::AddSeriesSelectQualityProfile => { + draw_confirmation_prompt(f, app, area); + draw_add_series_select_quality_profile_popup(f, app); + } + ActiveSonarrBlock::AddSeriesSelectLanguageProfile => { + draw_confirmation_prompt(f, app, area); + draw_add_series_select_language_profile_popup(f, app); + } + ActiveSonarrBlock::AddSeriesSelectRootFolder => { + draw_confirmation_prompt(f, app, area); + draw_add_series_select_root_folder_popup(f, app); + } + ActiveSonarrBlock::AddSeriesPrompt | ActiveSonarrBlock::AddSeriesTagsInput => { + draw_confirmation_prompt(f, app, area) + } + _ => (), + } + } +} + +fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let (series_title, series_overview) = ( + &app + .data + .sonarr_data + .add_searched_series + .as_ref() + .unwrap() + .current_selection() + .title + .text, + app + .data + .sonarr_data + .add_searched_series + .as_ref() + .unwrap() + .current_selection() + .overview + .clone() + .unwrap_or_default(), + ); + let title = format!("Add Series - {series_title}"); + let prompt = series_overview; + let yes_no_value = app.data.sonarr_data.prompt_confirm; + let selected_block = app.data.sonarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveSonarrBlock::AddSeriesConfirmPrompt; + let AddSeriesModal { + monitor_list, + series_type_list, + quality_profile_list, + language_profile_list, + root_folder_list, + use_season_folder, + tags, + .. + } = app.data.sonarr_data.add_series_modal.as_ref().unwrap(); + + let selected_monitor = monitor_list.current_selection(); + let selected_series_type = series_type_list.current_selection(); + let selected_quality_profile = quality_profile_list.current_selection(); + let selected_language_profile = language_profile_list.current_selection(); + let selected_root_folder = root_folder_list.current_selection(); + + f.render_widget(title_block_centered(&title), area); + + let [paragraph_area, root_folder_area, monitor_area, quality_profile_area, language_profile_area, series_type_area, season_folder_area, tags_area, _, buttons_area, help_area] = + Layout::vertical([ + Constraint::Length(6), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(1), + ]) + .margin(1) + .areas(area); + + let prompt_paragraph = layout_paragraph_borderless(&prompt); + let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); + let help_paragraph = Paragraph::new(help_text).centered(); + f.render_widget(prompt_paragraph, paragraph_area); + f.render_widget(help_paragraph, help_area); + + let [add_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(buttons_area); + + let use_season_folder_checkbox = Checkbox::new("Season Folder") + .checked(*use_season_folder) + .highlighted(selected_block == ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder); + let root_folder_drop_down_button = Button::new() + .title(&selected_root_folder.path) + .label("Root Folder") + .icon("▼") + .selected(selected_block == ActiveSonarrBlock::AddSeriesSelectRootFolder); + let monitor_drop_down_button = Button::new() + .title(selected_monitor.to_display_str()) + .label("Monitor") + .icon("▼") + .selected(selected_block == ActiveSonarrBlock::AddSeriesSelectMonitor); + let series_type_drop_down_button = Button::new() + .title(selected_series_type.to_display_str()) + .label("Series Type") + .icon("▼") + .selected(selected_block == ActiveSonarrBlock::AddSeriesSelectSeriesType); + let quality_profile_drop_down_button = Button::new() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == ActiveSonarrBlock::AddSeriesSelectQualityProfile); + let language_profile_drop_down_button = Button::new() + .title(selected_language_profile) + .label("Language Profile") + .icon("▼") + .selected(selected_block == ActiveSonarrBlock::AddSeriesSelectLanguageProfile); + + f.render_widget(root_folder_drop_down_button, root_folder_area); + f.render_widget(monitor_drop_down_button, monitor_area); + f.render_widget(quality_profile_drop_down_button, quality_profile_area); + f.render_widget(language_profile_drop_down_button, language_profile_area); + f.render_widget(series_type_drop_down_button, series_type_area); + f.render_widget(use_season_folder_checkbox, season_folder_area); + + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let tags_input_box = InputBox::new(&tags.text) + .offset(tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveSonarrBlock::AddSeriesTagsInput) + .selected(active_sonarr_block == ActiveSonarrBlock::AddSeriesTagsInput); + render_selectable_input_box!(tags_input_box, f, tags_area); + } + + let add_button = Button::new() + .title("Add") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(add_button, add_area); + f.render_widget(cancel_button, cancel_area); +} + +fn draw_add_series_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let monitor_list = SelectableList::new( + &mut app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .monitor_list, + |monitor| ListItem::new(monitor.to_display_str().to_owned()), + ); + let popup = Popup::new(monitor_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_series_select_series_type_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let series_type_list = SelectableList::new( + &mut app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .series_type_list, + |series_type| ListItem::new(series_type.to_display_str().to_owned()), + ); + let popup = Popup::new(series_type_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_series_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let quality_profile_list = SelectableList::new( + &mut app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .quality_profile_list, + |quality_profile| ListItem::new(quality_profile.clone()), + ); + let popup = Popup::new(quality_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_series_select_language_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let language_profile_list = SelectableList::new( + &mut app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .language_profile_list, + |language_profile| ListItem::new(language_profile.clone()), + ); + let popup = Popup::new(language_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_add_series_select_root_folder_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let root_folder_list = SelectableList::new( + &mut app + .data + .sonarr_data + .add_series_modal + .as_mut() + .unwrap() + .root_folder_list, + |root_folder| ListItem::new(root_folder.path.to_owned()), + ); + let popup = Popup::new(root_folder_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} diff --git a/src/ui/sonarr_ui/library/add_series_ui_tests.rs b/src/ui/sonarr_ui/library/add_series_ui_tests.rs new file mode 100644 index 0000000..6bd15cd --- /dev/null +++ b/src/ui/sonarr_ui/library/add_series_ui_tests.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS}; + use crate::ui::sonarr_ui::library::add_series_ui::AddSeriesUi; + use crate::ui::DrawUi; + + #[test] + fn test_add_series_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if ADD_SERIES_BLOCKS.contains(&active_sonarr_block) { + assert!(AddSeriesUi::accepts(active_sonarr_block.into())); + } else { + assert!(!AddSeriesUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/library/delete_series_ui.rs b/src/ui/sonarr_ui/library/delete_series_ui.rs new file mode 100644 index 0000000..13e8ebb --- /dev/null +++ b/src/ui/sonarr_ui/library/delete_series_ui.rs @@ -0,0 +1,58 @@ +use ratatui::layout::Rect; +use ratatui::Frame; + +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}; +use crate::models::Route; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; + +#[cfg(test)] +#[path = "delete_series_ui_tests.rs"] +mod delete_series_ui_tests; + +pub(super) struct DeleteSeriesUi; + +impl DrawUi for DeleteSeriesUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return DELETE_SERIES_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if matches!( + app.get_current_route(), + Route::Sonarr(ActiveSonarrBlock::DeleteSeriesPrompt, _) + ) { + let selected_block = app.data.sonarr_data.selected_block.get_active_block(); + let prompt = format!( + "Do you really want to delete the series: \n{}?", + app.data.sonarr_data.series.current_selection().title.text + ); + let checkboxes = vec![ + Checkbox::new("Delete Series File") + .checked(app.data.sonarr_data.delete_series_files) + .highlighted(selected_block == ActiveSonarrBlock::DeleteSeriesToggleDeleteFile), + Checkbox::new("Add List Exclusion") + .checked(app.data.sonarr_data.add_list_exclusion) + .highlighted(selected_block == ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion), + ]; + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Series") + .prompt(&prompt) + .checkboxes(checkboxes) + .yes_no_highlighted(selected_block == ActiveSonarrBlock::DeleteSeriesConfirmPrompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + } +} diff --git a/src/ui/sonarr_ui/library/delete_series_ui_tests.rs b/src/ui/sonarr_ui/library/delete_series_ui_tests.rs new file mode 100644 index 0000000..17c61b1 --- /dev/null +++ b/src/ui/sonarr_ui/library/delete_series_ui_tests.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}; + use crate::ui::sonarr_ui::library::delete_series_ui::DeleteSeriesUi; + use crate::ui::DrawUi; + + #[test] + fn test_delete_series_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if DELETE_SERIES_BLOCKS.contains(&active_sonarr_block) { + assert!(DeleteSeriesUi::accepts(active_sonarr_block.into())); + } else { + assert!(!DeleteSeriesUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/library/edit_series_ui.rs b/src/ui/sonarr_ui/library/edit_series_ui.rs new file mode 100644 index 0000000..74095da --- /dev/null +++ b/src/ui/sonarr_ui/library/edit_series_ui.rs @@ -0,0 +1,240 @@ +use std::sync::atomic::Ordering; + +use ratatui::layout::{Constraint, Rect}; +use ratatui::prelude::Layout; +use ratatui::text::Text; +use ratatui::widgets::{ListItem, Paragraph}; +use ratatui::Frame; + +use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; +use crate::app::App; +use crate::models::servarr_data::sonarr::modals::EditSeriesModal; +use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EDIT_SERIES_BLOCKS, SERIES_DETAILS_BLOCKS, +}; +use crate::models::{EnumDisplayStyle, Route}; +use crate::render_selectable_input_box; + +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::ui::{draw_popup, DrawUi}; + +use super::series_details_ui::SeriesDetailsUi; + +#[cfg(test)] +#[path = "edit_series_ui_tests.rs"] +mod edit_series_ui_tests; + +pub(super) struct EditSeriesUi; + +impl DrawUi for EditSeriesUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return EDIT_SERIES_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Sonarr(active_sonarr_block, context_option) = app.get_current_route() { + if let Some(context) = context_option { + if SERIES_DETAILS_BLOCKS.contains(&context) { + draw_popup(f, app, SeriesDetailsUi::draw, Size::Large); + } + } + + let draw_edit_series_prompt = |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| { + draw_edit_series_confirmation_prompt(f, app, prompt_area); + + match active_sonarr_block { + ActiveSonarrBlock::EditSeriesSelectSeriesType => { + draw_edit_series_select_series_type_popup(f, app); + } + ActiveSonarrBlock::EditSeriesSelectQualityProfile => { + draw_edit_series_select_quality_profile_popup(f, app); + } + ActiveSonarrBlock::EditSeriesSelectLanguageProfile => { + draw_edit_series_select_language_profile_popup(f, app); + } + _ => (), + } + }; + + draw_popup(f, app, draw_edit_series_prompt, Size::Long); + } + } +} + +fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let series_title = app + .data + .sonarr_data + .series + .current_selection() + .title + .text + .clone(); + let series_overview = app + .data + .sonarr_data + .series + .current_selection() + .overview + .clone() + .unwrap_or_default(); + let title = format!("Edit - {series_title}"); + let yes_no_value = app.data.sonarr_data.prompt_confirm; + let selected_block = app.data.sonarr_data.selected_block.get_active_block(); + let highlight_yes_no = selected_block == ActiveSonarrBlock::EditSeriesConfirmPrompt; + let EditSeriesModal { + series_type_list, + quality_profile_list, + language_profile_list, + monitored, + use_season_folders, + path, + tags, + } = app.data.sonarr_data.edit_series_modal.as_ref().unwrap(); + let selected_series_type = series_type_list.current_selection(); + let selected_quality_profile = quality_profile_list.current_selection(); + let selected_language_profile = language_profile_list.current_selection(); + + let [paragraph_area, monitored_area, season_folder_area, quality_profile_area, language_profile_area, series_type_area, path_area, tags_area, _, buttons_area, help_area] = + Layout::vertical([ + Constraint::Length(6), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Length(1), + ]) + .margin(1) + .areas(area); + let [save_area, cancel_area] = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(buttons_area); + + let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); + let help_paragraph = Paragraph::new(help_text).centered(); + let prompt_paragraph = layout_paragraph_borderless(&series_overview); + let monitored_checkbox = Checkbox::new("Monitored") + .checked(monitored.unwrap_or_default()) + .highlighted(selected_block == ActiveSonarrBlock::EditSeriesToggleMonitored); + let season_folder_checkbox = Checkbox::new("Season Folder") + .checked(use_season_folders.unwrap_or_default()) + .highlighted(selected_block == ActiveSonarrBlock::EditSeriesToggleSeasonFolder); + let series_type_drop_down_button = Button::new() + .title(selected_series_type.to_display_str()) + .label("Series Type") + .icon("▼") + .selected(selected_block == ActiveSonarrBlock::EditSeriesSelectSeriesType); + let quality_profile_drop_down_button = Button::new() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == ActiveSonarrBlock::EditSeriesSelectQualityProfile); + let language_profile_drop_down_button = Button::new() + .title(selected_language_profile) + .label("Language Profile") + .icon("▼") + .selected(selected_block == ActiveSonarrBlock::EditSeriesSelectLanguageProfile); + + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let path_input_box = InputBox::new(&path.text) + .offset(path.offset.load(Ordering::SeqCst)) + .label("Path") + .highlighted(selected_block == ActiveSonarrBlock::EditSeriesPathInput) + .selected(active_sonarr_block == ActiveSonarrBlock::EditSeriesPathInput); + let tags_input_box = InputBox::new(&tags.text) + .offset(tags.offset.load(Ordering::SeqCst)) + .label("Tags") + .highlighted(selected_block == ActiveSonarrBlock::EditSeriesTagsInput) + .selected(active_sonarr_block == ActiveSonarrBlock::EditSeriesTagsInput); + + match active_sonarr_block { + ActiveSonarrBlock::EditSeriesPathInput => path_input_box.show_cursor(f, path_area), + ActiveSonarrBlock::EditSeriesTagsInput => tags_input_box.show_cursor(f, tags_area), + _ => (), + } + + render_selectable_input_box!(path_input_box, f, path_area); + render_selectable_input_box!(tags_input_box, f, tags_area); + } + + let save_button = Button::new() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(title_block_centered(&title), area); + f.render_widget(prompt_paragraph, paragraph_area); + f.render_widget(monitored_checkbox, monitored_area); + f.render_widget(season_folder_checkbox, season_folder_area); + f.render_widget(quality_profile_drop_down_button, quality_profile_area); + f.render_widget(language_profile_drop_down_button, language_profile_area); + f.render_widget(series_type_drop_down_button, series_type_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); + f.render_widget(help_paragraph, help_area); +} + +fn draw_edit_series_select_series_type_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let series_type_list = SelectableList::new( + &mut app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .series_type_list, + |series_type| ListItem::new(series_type.to_display_str().to_owned()), + ); + let popup = Popup::new(series_type_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_edit_series_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let quality_profile_list = SelectableList::new( + &mut app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .quality_profile_list, + |quality_profile| ListItem::new(quality_profile.clone()), + ); + let popup = Popup::new(quality_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} + +fn draw_edit_series_select_language_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let language_profile_list = SelectableList::new( + &mut app + .data + .sonarr_data + .edit_series_modal + .as_mut() + .unwrap() + .language_profile_list, + |language_profile| ListItem::new(language_profile.clone()), + ); + let popup = Popup::new(language_profile_list).size(Size::Dropdown); + + f.render_widget(popup, f.area()); +} diff --git a/src/ui/sonarr_ui/library/edit_series_ui_tests.rs b/src/ui/sonarr_ui/library/edit_series_ui_tests.rs new file mode 100644 index 0000000..c138fa1 --- /dev/null +++ b/src/ui/sonarr_ui/library/edit_series_ui_tests.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_SERIES_BLOCKS}; + use crate::ui::sonarr_ui::library::edit_series_ui::EditSeriesUi; + use crate::ui::DrawUi; + + #[test] + fn test_edit_series_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if EDIT_SERIES_BLOCKS.contains(&active_sonarr_block) { + assert!(EditSeriesUi::accepts(active_sonarr_block.into())); + } else { + assert!(!EditSeriesUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/library/episode_details_ui.rs b/src/ui/sonarr_ui/library/episode_details_ui.rs new file mode 100644 index 0000000..b3b5f8a --- /dev/null +++ b/src/ui/sonarr_ui/library/episode_details_ui.rs @@ -0,0 +1,600 @@ +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS}; +use crate::models::sonarr_models::{ + DownloadRecord, DownloadStatus, Episode, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, +}; +use crate::models::Route; +use crate::ui::sonarr_ui::sonarr_ui_utils::{ + create_download_failed_history_event_details, + create_download_folder_imported_history_event_details, + create_episode_file_deleted_history_event_details, + create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, + create_no_data_history_event_details, +}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{ + borderless_block, decorate_peer_style, get_width_from_percentage, layout_block_bottom_border, + layout_block_top_border, +}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{draw_popup, draw_tabs, DrawUi}; +use crate::utils::convert_to_gb; +use chrono::Utc; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::{Line, Span, Text}; +use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; +use ratatui::Frame; + +#[cfg(test)] +#[path = "episode_details_ui_tests.rs"] +mod episode_details_ui_tests; + +pub(super) struct EpisodeDetailsUi; + +impl DrawUi for EpisodeDetailsUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return EPISODE_DETAILS_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Some(season_details_modal) = app.data.sonarr_data.season_details_modal.as_ref() { + if season_details_modal.episode_details_modal.is_some() { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let draw_episode_details_popup = + |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| { + let content_area = draw_tabs( + f, + popup_area, + "Episode Details", + &app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_details_tabs, + ); + draw_episode_details_tabs(f, app, content_area); + + match active_sonarr_block { + ActiveSonarrBlock::AutomaticallySearchEpisodePrompt => { + let prompt = format!( + "Do you want to trigger an automatic search of your indexers for the episode: {}", + app.data.sonarr_data.season_details_modal.as_ref().unwrap().episodes.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Automatic Episode Search") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt => { + draw_manual_episode_search_confirm_prompt(f, app); + } + ActiveSonarrBlock::EpisodeHistoryDetails => { + draw_history_item_details_popup(f, app, popup_area); + } + _ => (), + } + }; + + draw_popup(f, app, draw_episode_details_popup, Size::Large); + } + } + } + } +} + +pub fn draw_episode_details_tabs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Some(season_details_modal) = app.data.sonarr_data.season_details_modal.as_ref() { + if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() { + if let Route::Sonarr(active_sonarr_block, _) = episode_details_modal + .episode_details_tabs + .get_active_route() + { + match active_sonarr_block { + ActiveSonarrBlock::EpisodeDetails => draw_episode_details(f, app, area), + ActiveSonarrBlock::EpisodeHistory => draw_episode_history_table(f, app, area), + ActiveSonarrBlock::EpisodeFile => draw_file_info(f, app, area), + ActiveSonarrBlock::ManualEpisodeSearch => draw_episode_releases(f, app, area), + _ => (), + } + } + } + } +} + +fn draw_episode_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let block = layout_block_top_border(); + + match app.data.sonarr_data.season_details_modal.as_ref() { + Some(season_details_modal) if !app.is_loading => { + if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() { + let episode = season_details_modal.episodes.current_selection().clone(); + let episode_details = &episode_details_modal.episode_details; + let download = app + .data + .sonarr_data + .downloads + .items + .iter() + .find(|&download| download.episode_id == episode.id); + let text = Text::from( + episode_details + .items + .iter() + .map(|line| { + let split = line.split(':').collect::>(); + let title = format!("{}:", split[0]); + let style = style_from_status(download, &episode); + + Line::from(vec![ + title.bold().style(style), + Span::styled(split[1..].join(":"), style), + ]) + }) + .collect::>>(), + ); + + let paragraph = Paragraph::new(text) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((episode_details.offset, 0)); + + f.render_widget(paragraph, area); + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading + || app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .is_none(), + block, + ), + area, + ), + } +} + +fn draw_file_info(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + match app.data.sonarr_data.season_details_modal.as_ref() { + Some(season_details_modal) => match season_details_modal.episode_details_modal.as_ref() { + Some(episode_details_modal) + if !episode_details_modal.file_details.is_empty() && !app.is_loading => + { + let file_info = episode_details_modal.file_details.to_owned(); + let audio_details = episode_details_modal.audio_details.to_owned(); + let video_details = episode_details_modal.video_details.to_owned(); + let [file_details_title_area, file_details_area, audio_details_title_area, audio_details_area, video_details_title_area, video_details_area] = + Layout::vertical([ + Constraint::Length(2), + Constraint::Length(5), + Constraint::Length(1), + Constraint::Length(6), + Constraint::Length(1), + Constraint::Length(7), + ]) + .areas(area); + + let file_details_title_paragraph = + Paragraph::new("File Details".bold()).block(layout_block_top_border()); + let audio_details_title_paragraph = + Paragraph::new("Audio Details".bold()).block(borderless_block()); + let video_details_title_paragraph = + Paragraph::new("Video Details".bold()).block(borderless_block()); + + let file_details = Text::from(file_info); + let audio_details = Text::from(audio_details); + let video_details = Text::from(video_details); + + let file_details_paragraph = Paragraph::new(file_details) + .block(layout_block_bottom_border()) + .wrap(Wrap { trim: false }); + let audio_details_paragraph = Paragraph::new(audio_details) + .block(layout_block_bottom_border()) + .wrap(Wrap { trim: false }); + let video_details_paragraph = Paragraph::new(video_details) + .block(borderless_block()) + .wrap(Wrap { trim: false }); + + f.render_widget(file_details_title_paragraph, file_details_title_area); + f.render_widget(file_details_paragraph, file_details_area); + f.render_widget(audio_details_title_paragraph, audio_details_title_area); + f.render_widget(audio_details_paragraph, audio_details_area); + f.render_widget(video_details_title_paragraph, video_details_title_area); + f.render_widget(video_details_paragraph, video_details_area); + } + _ => f.render_widget(layout_block_top_border(), area), + }, + _ => f.render_widget( + LoadingBlock::new(app.is_loading, layout_block_top_border()), + area, + ), + } +} + +fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + match app.data.sonarr_data.season_details_modal.as_ref() { + Some(season_details_modal) if !app.is_loading => { + if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() { + let current_selection = if episode_details_modal.episode_history.is_empty() { + SonarrHistoryItem::default() + } else { + episode_details_modal + .episode_history + .current_selection() + .clone() + }; + let episode_history_table_footer = episode_details_modal + .episode_details_tabs + .get_active_tab_contextual_help(); + + let history_row_mapping = |history_item: &SonarrHistoryItem| { + let SonarrHistoryItem { + source_title, + languages, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 40), + current_selection == *history_item, + app.tick_count % app.ticks_until_scroll == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from( + languages + .iter() + .map(|language| language.name.to_owned()) + .collect::>() + .join(","), + ), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let mut episode_history_table = &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap() + .episode_history; + let history_table = + ManagarrTable::new(Some(&mut episode_history_table), history_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(episode_history_table_footer) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); + + f.render_widget(history_table, area); + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading + || app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .is_none(), + layout_block_top_border(), + ), + area, + ), + } +} + +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = + if let Some(season_details_modal) = app.data.sonarr_data.season_details_modal.as_ref() { + if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() { + if episode_details_modal.episode_history.is_empty() { + SonarrHistoryItem::default() + } else { + episode_details_modal + .episode_history + .current_selection() + .clone() + } + } else { + SonarrHistoryItem::default() + } + } else { + SonarrHistoryItem::default() + }; + + let line_vec = match current_selection.event_type { + SonarrHistoryEventType::Grabbed => create_grabbed_history_event_details(current_selection), + SonarrHistoryEventType::DownloadFolderImported => { + create_download_folder_imported_history_event_details(current_selection) + } + SonarrHistoryEventType::DownloadFailed => { + create_download_failed_history_event_details(current_selection) + } + SonarrHistoryEventType::EpisodeFileDeleted => { + create_episode_file_deleted_history_event_details(current_selection) + } + SonarrHistoryEventType::EpisodeFileRenamed => { + create_episode_file_renamed_history_event_details(current_selection) + } + _ => create_no_data_history_event_details(current_selection), + }; + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(Style::new().secondary()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowMessage), area); +} + +fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + match app.data.sonarr_data.season_details_modal.as_ref() { + Some(season_details_modal) if !app.is_loading => { + if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() { + let (current_selection, is_empty) = if episode_details_modal.episode_releases.is_empty() { + (SonarrRelease::default(), true) + } else { + ( + episode_details_modal + .episode_releases + .current_selection() + .clone(), + episode_details_modal.episode_releases.is_empty(), + ) + }; + let episode_release_table_footer = episode_details_modal + .episode_details_tabs + .get_active_tab_contextual_help(); + + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let episode_release_row_mapping = |release: &SonarrRelease| { + let SonarrRelease { + protocol, + age, + title, + indexer, + size, + rejected, + seeders, + leechers, + languages, + quality, + .. + } = release; + + let age = format!("{age} days"); + title.scroll_left_or_reset( + get_width_from_percentage(area, 30), + current_selection == *release + && active_sonarr_block != ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt, + app.tick_count % app.ticks_until_scroll == 0, + ); + let size = convert_to_gb(*size); + let rejected_str = if *rejected { "⛔" } else { "" }; + let peers = if seeders.is_none() || leechers.is_none() { + Text::from("") + } else { + let seeders = seeders.clone().unwrap().as_u64().unwrap(); + let leechers = leechers.clone().unwrap().as_u64().unwrap(); + + decorate_peer_style( + seeders, + leechers, + Text::from(format!("{seeders} / {leechers}")), + ) + }; + + let language = if languages.is_some() { + languages.clone().unwrap()[0].name.clone() + } else { + String::new() + }; + let quality = quality.quality.name.clone(); + + Row::new(vec![ + Cell::from(protocol.clone()), + Cell::from(age), + Cell::from(rejected_str), + Cell::from(title.to_string()), + Cell::from(indexer.clone()), + Cell::from(format!("{size:.1} GB")), + Cell::from(peers), + Cell::from(language), + Cell::from(quality), + ]) + .primary() + }; + let mut episode_release_table = &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap() + .episode_releases; + let release_table = ManagarrTable::new( + Some(&mut episode_release_table), + episode_release_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading || is_empty) + .footer(episode_release_table_footer) + .sorting(active_sonarr_block == ActiveSonarrBlock::ManualEpisodeSearchSortPrompt) + .headers([ + "Source", "Age", "⛔", "Title", "Indexer", "Size", "Peers", "Language", "Quality", + ]) + .constraints([ + Constraint::Length(9), + Constraint::Length(10), + Constraint::Length(5), + Constraint::Percentage(30), + Constraint::Percentage(18), + Constraint::Length(12), + Constraint::Length(12), + Constraint::Percentage(7), + Constraint::Percentage(10), + ]); + + f.render_widget(release_table, area); + } + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading + || app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .is_none(), + layout_block_top_border(), + ), + area, + ), + } +} + +fn draw_manual_episode_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_releases + .current_selection(); + let title = if current_selection.rejected { + "Download Rejected Release" + } else { + "Download Release" + }; + let prompt = if current_selection.rejected { + format!( + "Do you really want to download the rejected release: {}?", + ¤t_selection.title.text + ) + } else { + format!( + "Do you want to download the release: {}?", + ¤t_selection.title.text + ) + }; + + if current_selection.rejected { + let mut lines_vec = vec![Line::from("Rejection reasons: ".primary().bold())]; + let mut rejections_spans = current_selection + .rejections + .clone() + .unwrap_or_default() + .iter() + .map(|item| Line::from(format!("• {item}").primary().bold())) + .collect::>>(); + lines_vec.append(&mut rejections_spans); + + let content_paragraph = Paragraph::new(lines_vec) + .block(borderless_block()) + .wrap(Wrap { trim: false }) + .left_aligned(); + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .content(content_paragraph) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget(Popup::new(confirmation_prompt).size(Size::Small), f.area()); + } else { + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } +} + +fn style_from_status(download: Option<&DownloadRecord>, episode: &Episode) -> Style { + if !episode.has_file { + if let Some(download) = download { + if download.status == DownloadStatus::Downloading { + return Style::new().downloading(); + } + + if download.status == DownloadStatus::Completed { + return Style::new().awaiting_import(); + } + } + if !episode.monitored { + return Style::new().unmonitored_missing(); + } + + if let Some(air_date) = episode.air_date_utc.as_ref() { + if air_date > &Utc::now() { + return Style::new().unreleased(); + } + } + + return Style::new().missing(); + } + + if !episode.monitored { + Style::new().unmonitored() + } else { + Style::new().downloaded() + } +} diff --git a/src/ui/sonarr_ui/library/episode_details_ui_tests.rs b/src/ui/sonarr_ui/library/episode_details_ui_tests.rs new file mode 100644 index 0000000..dc8e654 --- /dev/null +++ b/src/ui/sonarr_ui/library/episode_details_ui_tests.rs @@ -0,0 +1,20 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS, + }; + use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi; + use crate::ui::DrawUi; + use strum::IntoEnumIterator; + + #[test] + fn test_episode_details_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if EPISODE_DETAILS_BLOCKS.contains(&active_sonarr_block) { + assert!(EpisodeDetailsUi::accepts(active_sonarr_block.into())); + } else { + assert!(!EpisodeDetailsUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/library/library_ui_tests.rs b/src/ui/sonarr_ui/library/library_ui_tests.rs new file mode 100644 index 0000000..a3c082b --- /dev/null +++ b/src/ui/sonarr_ui/library/library_ui_tests.rs @@ -0,0 +1,248 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, + EPISODE_DETAILS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS, + }; + use crate::models::{ + servarr_data::sonarr::sonarr_data::LIBRARY_BLOCKS, sonarr_models::SeriesStatus, + }; + use crate::ui::sonarr_ui::library::LibraryUi; + use crate::ui::styles::ManagarrStyle; + use crate::ui::DrawUi; + use pretty_assertions::assert_eq; + use ratatui::widgets::{Cell, Row}; + use strum::IntoEnumIterator; + + use crate::models::sonarr_models::{Season, SeasonStatistics}; + use crate::{ + models::sonarr_models::Series, ui::sonarr_ui::library::decorate_series_row_with_style, + }; + + #[test] + fn test_library_ui_accepts() { + let mut library_ui_blocks = Vec::new(); + library_ui_blocks.extend(LIBRARY_BLOCKS); + library_ui_blocks.extend(ADD_SERIES_BLOCKS); + library_ui_blocks.extend(DELETE_SERIES_BLOCKS); + library_ui_blocks.extend(EDIT_SERIES_BLOCKS); + library_ui_blocks.extend(SERIES_DETAILS_BLOCKS); + library_ui_blocks.extend(SEASON_DETAILS_BLOCKS); + library_ui_blocks.extend(EPISODE_DETAILS_BLOCKS); + + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if library_ui_blocks.contains(&active_sonarr_block) { + assert!(LibraryUi::accepts(active_sonarr_block.into())); + } else { + assert!(!LibraryUi::accepts(active_sonarr_block.into())); + } + }); + } + + #[test] + fn test_decorate_row_with_style_unmonitored() { + let series = Series::default(); + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.unmonitored()); + } + + #[test] + fn test_decorate_row_with_style_downloaded_when_ended_and_all_monitored_episodes_are_present() { + let seasons = vec![ + Season { + monitored: false, + statistics: SeasonStatistics { + episode_file_count: 1, + episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + Season { + monitored: true, + statistics: SeasonStatistics { + episode_file_count: 3, + episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + ]; + let series = Series { + monitored: true, + status: SeriesStatus::Ended, + seasons: Some(seasons), + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.downloaded()); + } + + #[test] + fn test_decorate_row_with_style_missing_when_ended_and_episodes_are_missing() { + let seasons = vec![ + Season { + monitored: true, + statistics: SeasonStatistics { + episode_file_count: 1, + episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + Season { + monitored: true, + statistics: SeasonStatistics { + episode_file_count: 3, + episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + ]; + let series = Series { + monitored: true, + status: SeriesStatus::Ended, + seasons: Some(seasons), + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_ended_and_seasons_is_empty() { + let series = Series { + monitored: true, + status: SeriesStatus::Ended, + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_unreleased_when_continuing_and_all_monitored_episodes_are_present( + ) { + let seasons = vec![ + Season { + monitored: false, + statistics: SeasonStatistics { + episode_file_count: 1, + episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + Season { + monitored: true, + statistics: SeasonStatistics { + episode_file_count: 3, + episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + ]; + let series = Series { + monitored: true, + status: SeriesStatus::Continuing, + seasons: Some(seasons), + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.unreleased()); + } + + #[test] + fn test_decorate_row_with_style_missing_when_continuing_and_episodes_are_missing() { + let seasons = vec![ + Season { + monitored: true, + statistics: SeasonStatistics { + episode_file_count: 1, + episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + Season { + monitored: true, + statistics: SeasonStatistics { + episode_file_count: 3, + episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + ]; + let series = Series { + monitored: true, + status: SeriesStatus::Continuing, + seasons: Some(seasons), + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_continuing_and_seasons_is_empty() { + let series = Series { + monitored: true, + status: SeriesStatus::Continuing, + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_unreleased_when_upcoming() { + let series = Series { + monitored: true, + status: SeriesStatus::Upcoming, + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.unreleased()); + } + + #[test] + fn test_decorate_row_with_style_defaults_to_indeterminate() { + let series = Series { + monitored: true, + status: SeriesStatus::Deleted, + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.indeterminate()); + } +} diff --git a/src/ui/sonarr_ui/library/mod.rs b/src/ui/sonarr_ui/library/mod.rs new file mode 100644 index 0000000..a29ec77 --- /dev/null +++ b/src/ui/sonarr_ui/library/mod.rs @@ -0,0 +1,233 @@ +use add_series_ui::AddSeriesUi; +use delete_series_ui::DeleteSeriesUi; +use edit_series_ui::EditSeriesUi; +use ratatui::{ + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; +use series_details_ui::SeriesDetailsUi; + +use crate::ui::widgets::{ + confirmation_prompt::ConfirmationPrompt, + popup::{Popup, Size}, +}; +use crate::{ + app::App, + models::{ + servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, LIBRARY_BLOCKS}, + sonarr_models::{Series, SeriesStatus}, + EnumDisplayStyle, Route, + }, + ui::{ + styles::ManagarrStyle, + utils::{get_width_from_percentage, layout_block_top_border}, + widgets::managarr_table::ManagarrTable, + DrawUi, + }, +}; + +mod add_series_ui; +mod delete_series_ui; +mod edit_series_ui; +mod series_details_ui; + +mod episode_details_ui; +#[cfg(test)] +#[path = "library_ui_tests.rs"] +mod library_ui_tests; +mod season_details_ui; + +pub(super) struct LibraryUi; + +impl DrawUi for LibraryUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return AddSeriesUi::accepts(route) + || DeleteSeriesUi::accepts(route) + || EditSeriesUi::accepts(route) + || SeriesDetailsUi::accepts(route) + || LIBRARY_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + draw_library(f, app, area); + + match route { + _ if AddSeriesUi::accepts(route) => AddSeriesUi::draw(f, app, area), + _ if DeleteSeriesUi::accepts(route) => DeleteSeriesUi::draw(f, app, area), + _ if EditSeriesUi::accepts(route) => EditSeriesUi::draw(f, app, area), + _ if SeriesDetailsUi::accepts(route) => SeriesDetailsUi::draw(f, app, area), + Route::Sonarr(ActiveSonarrBlock::UpdateAllSeriesPrompt, _) => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update All Series") + .prompt("Do you want to update info and scan your disks for all of your series?") + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + } + } +} + +fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let current_selection = if !app.data.sonarr_data.series.items.is_empty() { + app.data.sonarr_data.series.current_selection().clone() + } else { + Series::default() + }; + let quality_profile_map = &app.data.sonarr_data.quality_profile_map; + let language_profile_map = &app.data.sonarr_data.language_profiles_map; + let tags_map = &app.data.sonarr_data.tags_map; + let content = Some(&mut app.data.sonarr_data.series); + let help_footer = app + .data + .sonarr_data + .main_tabs + .get_active_tab_contextual_help(); + + let series_table_row_mapping = |series: &Series| { + series.title.scroll_left_or_reset( + get_width_from_percentage(area, 23), + *series == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); + let monitored = if series.monitored { "🏷" } else { "" }; + let certification = series.certification.clone().unwrap_or_default(); + let network = series.network.clone().unwrap_or_default(); + let quality_profile = quality_profile_map + .get_by_left(&series.quality_profile_id) + .unwrap() + .to_owned(); + let language_profile = language_profile_map + .get_by_left(&series.language_profile_id) + .unwrap() + .to_owned(); + let tags = if !series.tags.is_empty() { + series + .tags + .iter() + .map(|tag_id| { + tags_map + .get_by_left(&tag_id.as_i64().unwrap()) + .unwrap() + .clone() + }) + .collect::>() + .join(", ") + } else { + String::new() + }; + + decorate_series_row_with_style( + series, + Row::new(vec![ + Cell::from(series.title.to_string()), + Cell::from(series.year.to_string()), + Cell::from(network), + Cell::from(series.status.to_display_str()), + Cell::from(certification), + Cell::from(series.series_type.to_display_str()), + Cell::from(quality_profile), + Cell::from(language_profile), + Cell::from(monitored.to_owned()), + Cell::from(tags), + ]), + ) + }; + let series_table = ManagarrTable::new(content, series_table_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(help_footer) + .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt) + .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeries) + .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeries) + .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeriesError) + .filter_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::FilterSeriesError) + .headers([ + "Title", + "Year", + "Network", + "Status", + "Rating", + "Type", + "Quality Profile", + "Language Profile", + "Monitored", + "Tags", + ]) + .constraints([ + Constraint::Percentage(23), + Constraint::Percentage(4), + Constraint::Percentage(14), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(13), + Constraint::Percentage(10), + Constraint::Percentage(6), + Constraint::Percentage(12), + ]); + + if [ + ActiveSonarrBlock::SearchSeries, + ActiveSonarrBlock::FilterSeries, + ] + .contains(&active_sonarr_block) + { + series_table.show_cursor(f, area); + } + + f.render_widget(series_table, area); + } +} + +fn decorate_series_row_with_style<'a>(series: &Series, row: Row<'a>) -> Row<'a> { + if !series.monitored { + return row.unmonitored(); + } + + match series.status { + SeriesStatus::Ended => { + if let Some(ref seasons) = series.seasons { + return if seasons + .iter() + .filter(|season| season.monitored) + .all(|season| season.statistics.episode_file_count == season.statistics.episode_count) + { + row.downloaded() + } else { + row.missing() + }; + } + + row.indeterminate() + } + SeriesStatus::Continuing => { + if let Some(ref seasons) = series.seasons { + return if seasons + .iter() + .filter(|season| season.monitored) + .all(|season| season.statistics.episode_file_count == season.statistics.episode_count) + { + row.unreleased() + } else { + row.missing() + }; + } + + row.indeterminate() + } + SeriesStatus::Upcoming => row.unreleased(), + _ => row.indeterminate(), + } +} diff --git a/src/ui/sonarr_ui/library/season_details_ui.rs b/src/ui/sonarr_ui/library/season_details_ui.rs new file mode 100644 index 0000000..523d9ba --- /dev/null +++ b/src/ui/sonarr_ui/library/season_details_ui.rs @@ -0,0 +1,607 @@ +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS}; +use crate::models::sonarr_models::{ + DownloadRecord, DownloadStatus, Episode, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, +}; +use crate::models::Route; +use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi; +use crate::ui::sonarr_ui::sonarr_ui_utils::{ + create_download_failed_history_event_details, + create_download_folder_imported_history_event_details, + create_episode_file_deleted_history_event_details, + create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, + create_no_data_history_event_details, +}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{ + borderless_block, decorate_peer_style, get_width_from_percentage, layout_block_top_border, +}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{draw_popup, draw_tabs, DrawUi}; +use crate::utils::convert_to_gb; +use chrono::Utc; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::prelude::{Line, Style, Stylize, Text}; +use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; +use ratatui::Frame; + +#[cfg(test)] +#[path = "season_details_ui_tests.rs"] +mod season_details_ui_tests; + +pub(super) struct SeasonDetailsUi; + +impl DrawUi for SeasonDetailsUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return EpisodeDetailsUi::accepts(route) + || SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + let route = app.get_current_route(); + if app.data.sonarr_data.season_details_modal.is_some() { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let draw_season_details_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| { + let content_area = draw_tabs( + f, + popup_area, + &format!( + "Season {} Details", + app + .data + .sonarr_data + .seasons + .current_selection() + .season_number + ), + &app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_details_tabs, + ); + draw_season_details(f, app, content_area); + + match active_sonarr_block { + ActiveSonarrBlock::AutomaticallySearchSeasonPrompt => { + let prompt = format!( + "Do you want to trigger an automatic search of your indexers for season packs for: {}", + app.data.sonarr_data.seasons.current_selection().title.as_ref().unwrap() + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Automatic Season Search") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveSonarrBlock::DeleteEpisodeFilePrompt => { + let prompt = format!( + "Do you really want to delete this episode: \n{}?", + app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episodes + .current_selection() + .title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Episode") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt => { + draw_manual_season_search_confirm_prompt(f, app); + } + ActiveSonarrBlock::SeasonHistoryDetails => { + draw_history_item_details_popup(f, app, popup_area); + } + _ => (), + } + }; + + draw_popup(f, app, draw_season_details_popup, Size::XLarge); + + if EpisodeDetailsUi::accepts(route) { + EpisodeDetailsUi::draw(f, app, _area); + } + } + } + } +} + +pub fn draw_season_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Some(season_details_modal) = app.data.sonarr_data.season_details_modal.as_ref() { + if let Route::Sonarr(active_sonarr_block, _) = + season_details_modal.season_details_tabs.get_active_route() + { + match active_sonarr_block { + ActiveSonarrBlock::SeasonDetails => draw_episodes_table(f, app, area), + ActiveSonarrBlock::SeasonHistory => draw_season_history_table(f, app, area), + ActiveSonarrBlock::ManualSeasonSearch => draw_season_releases(f, app, area), + _ => (), + } + } + } +} + +fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let help_footer = app + .data + .sonarr_data + .season_details_modal + .as_ref() + .expect("Season details modal is unpopulated") + .season_details_tabs + .get_active_tab_contextual_help(); + let episode_files = app + .data + .sonarr_data + .season_details_modal + .as_ref() + .expect("Season details modal is unpopulated") + .episode_files + .items + .clone(); + let content = Some( + &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("Season details modal is unpopulated") + .episodes, + ); + let downloads_vec = &app.data.sonarr_data.downloads.items; + + let episode_row_mapping = |episode: &Episode| { + let Episode { + episode_number, + title, + air_date_utc, + episode_file_id, + .. + } = episode; + let episode_file = episode_files + .iter() + .find(|episode_file| episode_file.id == *episode_file_id); + let (quality_profile, size_on_disk) = if let Some(episode_file) = episode_file { + ( + episode_file.quality.quality.name.to_owned(), + episode_file.size, + ) + } else { + (String::new(), 0) + }; + + let episode_monitored = if episode.monitored { "🏷" } else { "" }; + let size = convert_to_gb(size_on_disk); + let air_date = if let Some(air_date) = air_date_utc.as_ref() { + air_date.to_string() + } else { + String::new() + }; + + decorate_with_row_style( + downloads_vec, + episode, + Row::new(vec![ + Cell::from(episode_monitored.to_owned()), + Cell::from(episode_number.to_string()), + Cell::from(title.clone()), + Cell::from(air_date), + Cell::from(format!("{size:.2} GB")), + Cell::from(quality_profile), + ]), + ) + }; + let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchEpisodes; + let season_table = ManagarrTable::new(content, episode_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(help_footer) + .searching(is_searching) + .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchEpisodesError) + .headers([ + "🏷", + "#", + "Title", + "Air Date", + "Size on Disk", + "Quality Profile", + ]) + .constraints([ + Constraint::Percentage(4), + Constraint::Percentage(4), + Constraint::Percentage(50), + Constraint::Percentage(19), + Constraint::Percentage(10), + Constraint::Percentage(12), + ]); + + if is_searching { + season_table.show_cursor(f, area); + } + + f.render_widget(season_table, area); + } +} + +fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + match app.data.sonarr_data.season_details_modal.as_ref() { + Some(season_details_modal) if !app.is_loading => { + let current_selection = if season_details_modal.season_history.is_empty() { + SonarrHistoryItem::default() + } else { + season_details_modal + .season_history + .current_selection() + .clone() + }; + let season_history_table_footer = season_details_modal + .season_details_tabs + .get_active_tab_contextual_help(); + + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let history_row_mapping = |history_item: &SonarrHistoryItem| { + let SonarrHistoryItem { + source_title, + languages, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 40), + current_selection == *history_item, + app.tick_count % app.ticks_until_scroll == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from( + languages + .iter() + .map(|language| language.name.to_owned()) + .collect::>() + .join(","), + ), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let mut season_history_table = &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history; + let history_table = + ManagarrTable::new(Some(&mut season_history_table), history_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(season_history_table_footer) + .sorting(active_sonarr_block == ActiveSonarrBlock::SeasonHistorySortPrompt) + .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistory) + .search_produced_empty_results( + active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistoryError, + ) + .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeasonHistory) + .filter_produced_empty_results( + active_sonarr_block == ActiveSonarrBlock::FilterSeasonHistoryError, + ) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); + + if [ + ActiveSonarrBlock::SearchSeriesHistory, + ActiveSonarrBlock::FilterSeriesHistory, + ] + .contains(&active_sonarr_block) + { + history_table.show_cursor(f, area); + } + + f.render_widget(history_table, area); + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading || app.data.sonarr_data.season_details_modal.is_none(), + layout_block_top_border(), + ), + area, + ), + } +} + +fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + match app.data.sonarr_data.season_details_modal.as_ref() { + Some(season_details_modal) if !app.is_loading => { + let (current_selection, is_empty) = if season_details_modal.season_releases.is_empty() { + (SonarrRelease::default(), true) + } else { + ( + season_details_modal + .season_releases + .current_selection() + .clone(), + season_details_modal.season_releases.is_empty(), + ) + }; + let season_release_table_footer = season_details_modal + .season_details_tabs + .get_active_tab_contextual_help(); + + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let season_release_row_mapping = |release: &SonarrRelease| { + let SonarrRelease { + protocol, + age, + title, + indexer, + size, + rejected, + seeders, + leechers, + languages, + quality, + .. + } = release; + + let age = format!("{age} days"); + title.scroll_left_or_reset( + get_width_from_percentage(area, 30), + current_selection == *release + && active_sonarr_block != ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, + app.tick_count % app.ticks_until_scroll == 0, + ); + let size = convert_to_gb(*size); + let rejected_str = if *rejected { "⛔" } else { "" }; + let peers = if seeders.is_none() || leechers.is_none() { + Text::from("") + } else { + let seeders = seeders.clone().unwrap().as_u64().unwrap(); + let leechers = leechers.clone().unwrap().as_u64().unwrap(); + + decorate_peer_style( + seeders, + leechers, + Text::from(format!("{seeders} / {leechers}")), + ) + }; + + let language = if languages.is_some() { + languages.clone().unwrap()[0].name.clone() + } else { + String::new() + }; + let quality = quality.quality.name.clone(); + + Row::new(vec![ + Cell::from(protocol.clone()), + Cell::from(age), + Cell::from(rejected_str), + Cell::from(title.to_string()), + Cell::from(indexer.clone()), + Cell::from(format!("{size:.1} GB")), + Cell::from(peers), + Cell::from(language), + Cell::from(quality), + ]) + .primary() + }; + let mut season_release_table = &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_releases; + let release_table = + ManagarrTable::new(Some(&mut season_release_table), season_release_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading || is_empty) + .footer(season_release_table_footer) + .sorting(active_sonarr_block == ActiveSonarrBlock::ManualSeasonSearchSortPrompt) + .headers([ + "Source", "Age", "⛔", "Title", "Indexer", "Size", "Peers", "Language", "Quality", + ]) + .constraints([ + Constraint::Length(9), + Constraint::Length(10), + Constraint::Length(5), + Constraint::Percentage(30), + Constraint::Percentage(18), + Constraint::Length(12), + Constraint::Length(12), + Constraint::Percentage(7), + Constraint::Percentage(10), + ]); + + f.render_widget(release_table, area); + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading || app.data.sonarr_data.season_details_modal.is_none(), + layout_block_top_border(), + ), + area, + ), + } +} + +fn draw_manual_season_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_releases + .current_selection(); + let title = if current_selection.rejected { + "Download Rejected Release" + } else { + "Download Release" + }; + let prompt = if current_selection.rejected { + format!( + "Do you really want to download the rejected release: {}?", + ¤t_selection.title.text + ) + } else { + format!( + "Do you want to download the release: {}?", + ¤t_selection.title.text + ) + }; + + if current_selection.rejected { + let mut lines_vec = vec![Line::from("Rejection reasons: ".primary().bold())]; + let mut rejections_spans = current_selection + .rejections + .clone() + .unwrap_or_default() + .iter() + .map(|item| Line::from(format!("• {item}").primary().bold())) + .collect::>>(); + lines_vec.append(&mut rejections_spans); + + let content_paragraph = Paragraph::new(lines_vec) + .block(borderless_block()) + .wrap(Wrap { trim: false }) + .left_aligned(); + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .content(content_paragraph) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget(Popup::new(confirmation_prompt).size(Size::Small), f.area()); + } else { + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } +} + +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = + if let Some(season_details_modal) = app.data.sonarr_data.season_details_modal.as_ref() { + if season_details_modal.season_history.is_empty() { + SonarrHistoryItem::default() + } else { + season_details_modal + .season_history + .current_selection() + .clone() + } + } else { + SonarrHistoryItem::default() + }; + + let line_vec = match current_selection.event_type { + SonarrHistoryEventType::Grabbed => create_grabbed_history_event_details(current_selection), + SonarrHistoryEventType::DownloadFolderImported => { + create_download_folder_imported_history_event_details(current_selection) + } + SonarrHistoryEventType::DownloadFailed => { + create_download_failed_history_event_details(current_selection) + } + SonarrHistoryEventType::EpisodeFileDeleted => { + create_episode_file_deleted_history_event_details(current_selection) + } + SonarrHistoryEventType::EpisodeFileRenamed => { + create_episode_file_renamed_history_event_details(current_selection) + } + _ => create_no_data_history_event_details(current_selection), + }; + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(Style::new().secondary()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowMessage), area); +} + +fn decorate_with_row_style<'a>( + downloads_vec: &[DownloadRecord], + episode: &Episode, + row: Row<'a>, +) -> Row<'a> { + if !episode.has_file { + if let Some(download) = downloads_vec + .iter() + .find(|&download| download.episode_id == episode.id) + { + if download.status == DownloadStatus::Downloading { + return row.downloading(); + } + + if download.status == DownloadStatus::Completed { + return row.awaiting_import(); + } + } + + if !episode.monitored { + return row.unmonitored_missing(); + } + + if let Some(air_date) = episode.air_date_utc.as_ref() { + if air_date > &Utc::now() { + return row.unreleased(); + } + } + + return row.missing(); + } + + if !episode.monitored { + row.unmonitored() + } else { + row.downloaded() + } +} diff --git a/src/ui/sonarr_ui/library/season_details_ui_tests.rs b/src/ui/sonarr_ui/library/season_details_ui_tests.rs new file mode 100644 index 0000000..7145fbc --- /dev/null +++ b/src/ui/sonarr_ui/library/season_details_ui_tests.rs @@ -0,0 +1,24 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS, SEASON_DETAILS_BLOCKS, + }; + use crate::ui::sonarr_ui::library::season_details_ui::SeasonDetailsUi; + use crate::ui::DrawUi; + + #[test] + fn test_season_details_ui_accepts() { + let mut blocks = SEASON_DETAILS_BLOCKS.clone().to_vec(); + blocks.extend(EPISODE_DETAILS_BLOCKS); + + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if blocks.contains(&active_sonarr_block) { + assert!(SeasonDetailsUi::accepts(active_sonarr_block.into())); + } else { + assert!(!SeasonDetailsUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/library/series_details_ui.rs b/src/ui/sonarr_ui/library/series_details_ui.rs new file mode 100644 index 0000000..ddeee4e --- /dev/null +++ b/src/ui/sonarr_ui/library/series_details_ui.rs @@ -0,0 +1,425 @@ +use chrono::Utc; +use deunicode::deunicode; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::{Line, Text}; +use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; +use ratatui::Frame; +use regex::Regex; + +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS}; +use crate::models::sonarr_models::{ + Season, SeasonStatistics, SonarrHistoryEventType, SonarrHistoryItem, +}; +use crate::models::{EnumDisplayStyle, Route}; +use crate::ui::sonarr_ui::library::episode_details_ui::EpisodeDetailsUi; +use crate::ui::sonarr_ui::library::season_details_ui::SeasonDetailsUi; +use crate::ui::sonarr_ui::sonarr_ui_utils::{ + create_download_failed_history_event_details, + create_download_folder_imported_history_event_details, + create_episode_file_deleted_history_event_details, + create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, + create_no_data_history_event_details, +}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{ + borderless_block, get_width_from_percentage, layout_block_top_border, title_block, +}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{draw_popup, draw_tabs, DrawUi}; +use crate::utils::convert_to_gb; + +#[cfg(test)] +#[path = "series_details_ui_tests.rs"] +mod series_details_ui_tests; + +pub(super) struct SeriesDetailsUi; + +impl DrawUi for SeriesDetailsUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return SeasonDetailsUi::accepts(route) + || EpisodeDetailsUi::accepts(route) + || SERIES_DETAILS_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + if let Route::Sonarr(active_sonarr_block, _) = route { + let draw_series_details_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| { + f.render_widget( + title_block(&app.data.sonarr_data.series.current_selection().title.text), + popup_area, + ); + let [description_area, detail_area] = + Layout::vertical([Constraint::Percentage(37), Constraint::Fill(0)]) + .margin(1) + .areas(popup_area); + draw_series_description(f, app, description_area); + let content_area = draw_tabs( + f, + detail_area, + "Series Details", + &app.data.sonarr_data.series_info_tabs, + ); + draw_series_details(f, app, content_area); + + match active_sonarr_block { + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + let prompt = format!( + "Do you want to trigger an automatic search of your indexers for all monitored episode(s) for the series: {}", app.data.sonarr_data.series.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Automatic Series Search") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { + let prompt = format!( + "Do you want to trigger an update and disk scan for the series: {}?", + app.data.sonarr_data.series.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update and Scan") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveSonarrBlock::SeriesHistoryDetails => { + draw_history_item_details_popup(f, app, popup_area); + } + _ => (), + }; + }; + + draw_popup(f, app, draw_series_details_popup, Size::XXLarge); + + if SeasonDetailsUi::accepts(route) { + SeasonDetailsUi::draw(f, app, area); + } + } + } +} + +fn draw_series_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = app.data.sonarr_data.series.current_selection(); + let monitored = if current_selection.monitored { + "Yes" + } else { + "No" + }; + let quality_profile = app + .data + .sonarr_data + .quality_profile_map + .get_by_left(¤t_selection.quality_profile_id) + .unwrap() + .to_owned(); + let language_profile = app + .data + .sonarr_data + .language_profiles_map + .get_by_left(¤t_selection.language_profile_id) + .unwrap() + .to_owned(); + let overview = Regex::new(r"[\r\n\t]") + .unwrap() + .replace_all( + &deunicode( + current_selection + .overview + .as_ref() + .unwrap_or(&String::new()), + ), + "", + ) + .to_string(); + + let mut series_description = vec![ + Line::from(vec![ + "Title: ".primary().bold(), + current_selection.title.text.clone().primary().bold(), + ]), + Line::from(vec!["Overview: ".primary().bold(), overview.default()]), + Line::from(vec![ + "Network: ".primary().bold(), + current_selection + .network + .clone() + .unwrap_or_default() + .default(), + ]), + Line::from(vec![ + "Status: ".primary().bold(), + current_selection.status.to_display_str().default(), + ]), + Line::from(vec![ + "Genres: ".primary().bold(), + current_selection.genres.join(", ").default(), + ]), + Line::from(vec![ + "Rating: ".primary().bold(), + format!("{}%", (current_selection.ratings.value * 10.0) as i32).default(), + ]), + Line::from(vec![ + "Year: ".primary().bold(), + current_selection.year.to_string().default(), + ]), + Line::from(vec![ + "Runtime: ".primary().bold(), + format!("{} minutes", current_selection.runtime).default(), + ]), + Line::from(vec![ + "Path: ".primary().bold(), + current_selection.path.clone().default(), + ]), + Line::from(vec![ + "Quality Profile: ".primary().bold(), + quality_profile.default(), + ]), + Line::from(vec![ + "Language Profile: ".primary().bold(), + language_profile.default(), + ]), + Line::from(vec!["Monitored: ".primary().bold(), monitored.default()]), + ]; + if let Some(stats) = current_selection.statistics.as_ref() { + let size = convert_to_gb(stats.size_on_disk); + series_description.extend(vec![Line::from(vec![ + "Size on Disk: ".primary().bold(), + format!("{size:.2} GB").default(), + ])]); + } + + let description_paragraph = Paragraph::new(series_description) + .block(borderless_block()) + .wrap(Wrap { trim: true }); + f.render_widget(description_paragraph, area); +} + +pub fn draw_series_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = + app.data.sonarr_data.series_info_tabs.get_active_route() + { + match active_sonarr_block { + ActiveSonarrBlock::SeriesDetails => draw_seasons_table(f, app, area), + ActiveSonarrBlock::SeriesHistory => draw_series_history_table(f, app, area), + _ => (), + } + } +} + +fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let content = Some(&mut app.data.sonarr_data.seasons); + let help_footer = app + .data + .sonarr_data + .series_info_tabs + .get_active_tab_contextual_help(); + let season_row_mapping = |season: &Season| { + let Season { + title, + monitored, + statistics, + .. + } = season; + let SeasonStatistics { + episode_file_count, + episode_count, + size_on_disk, + next_airing, + .. + } = statistics; + let season_monitored = if season.monitored { "🏷" } else { "" }; + let size = convert_to_gb(*size_on_disk); + + let row = Row::new(vec![ + Cell::from(season_monitored.to_owned()), + Cell::from(title.clone().unwrap()), + Cell::from(format!("{}/{}", episode_file_count, episode_count)), + Cell::from(format!("{size:.2} GB")), + ]); + if !monitored { + row.unmonitored() + } else if episode_file_count == episode_count { + row.downloaded() + } else if let Some(next_airing_utc) = next_airing.as_ref() { + if next_airing_utc > &Utc::now() { + return row.unreleased(); + } else { + return row.missing(); + } + } else { + row.missing() + } + }; + let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchSeason; + let season_table = ManagarrTable::new(content, season_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(help_footer) + .searching(is_searching) + .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeasonError) + .headers(["Monitored", "Season", "Episode Count", "Size on Disk"]) + .constraints([ + Constraint::Percentage(6), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ]); + + if is_searching { + season_table.show_cursor(f, area); + } + + f.render_widget(season_table, area); + } +} + +fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + match app.data.sonarr_data.series_history.as_ref() { + Some(series_history) if !app.is_loading => { + let current_selection = if series_history.is_empty() { + SonarrHistoryItem::default() + } else { + series_history.current_selection().clone() + }; + let series_history_table_footer = app + .data + .sonarr_data + .series_info_tabs + .get_active_tab_contextual_help(); + + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + let history_row_mapping = |history_item: &SonarrHistoryItem| { + let SonarrHistoryItem { + source_title, + languages, + quality, + event_type, + date, + .. + } = history_item; + + source_title.scroll_left_or_reset( + get_width_from_percentage(area, 40), + current_selection == *history_item, + app.tick_count % app.ticks_until_scroll == 0, + ); + + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_string()), + Cell::from( + languages + .iter() + .map(|language| language.name.to_owned()) + .collect::>() + .join(","), + ), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let mut series_history_table = app.data.sonarr_data.series_history.as_mut().unwrap(); + let history_table = + ManagarrTable::new(Some(&mut series_history_table), history_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(series_history_table_footer) + .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesHistorySortPrompt) + .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistory) + .search_produced_empty_results( + active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistoryError, + ) + .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeriesHistory) + .filter_produced_empty_results( + active_sonarr_block == ActiveSonarrBlock::FilterSeriesHistoryError, + ) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); + + if [ + ActiveSonarrBlock::SearchSeriesHistory, + ActiveSonarrBlock::FilterSeriesHistory, + ] + .contains(&active_sonarr_block) + { + history_table.show_cursor(f, area); + } + + f.render_widget(history_table, area); + } + } + _ => f.render_widget( + LoadingBlock::new( + app.is_loading || app.data.sonarr_data.seasons.is_empty(), + layout_block_top_border(), + ), + area, + ), + } +} + +fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = + if let Some(series_history_items) = app.data.sonarr_data.series_history.as_ref() { + if series_history_items.is_empty() { + SonarrHistoryItem::default() + } else { + series_history_items.current_selection().clone() + } + } else { + SonarrHistoryItem::default() + }; + + let line_vec = match current_selection.event_type { + SonarrHistoryEventType::Grabbed => create_grabbed_history_event_details(current_selection), + SonarrHistoryEventType::DownloadFolderImported => { + create_download_folder_imported_history_event_details(current_selection) + } + SonarrHistoryEventType::DownloadFailed => { + create_download_failed_history_event_details(current_selection) + } + SonarrHistoryEventType::EpisodeFileDeleted => { + create_episode_file_deleted_history_event_details(current_selection) + } + SonarrHistoryEventType::EpisodeFileRenamed => { + create_episode_file_renamed_history_event_details(current_selection) + } + _ => create_no_data_history_event_details(current_selection), + }; + let text = Text::from(line_vec); + + let message = Message::new(text) + .title("Details") + .style(Style::new().secondary()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowMessage), area); +} diff --git a/src/ui/sonarr_ui/library/series_details_ui_tests.rs b/src/ui/sonarr_ui/library/series_details_ui_tests.rs new file mode 100644 index 0000000..9923c87 --- /dev/null +++ b/src/ui/sonarr_ui/library/series_details_ui_tests.rs @@ -0,0 +1,25 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS, + }; + use crate::ui::sonarr_ui::library::series_details_ui::SeriesDetailsUi; + use crate::ui::DrawUi; + + #[test] + fn test_series_details_ui_accepts() { + let mut blocks = SERIES_DETAILS_BLOCKS.clone().to_vec(); + blocks.extend(SEASON_DETAILS_BLOCKS); + blocks.extend(EPISODE_DETAILS_BLOCKS); + + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if blocks.contains(&active_sonarr_block) { + assert!(SeriesDetailsUi::accepts(active_sonarr_block.into())); + } else { + assert!(!SeriesDetailsUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/mod.rs b/src/ui/sonarr_ui/mod.rs new file mode 100644 index 0000000..845daea --- /dev/null +++ b/src/ui/sonarr_ui/mod.rs @@ -0,0 +1,228 @@ +use std::{cmp, iter}; + +use blocklist::BlocklistUi; +use chrono::{Duration, Utc}; +use downloads::DownloadsUi; +use history::HistoryUi; +use indexers::IndexersUi; +use library::LibraryUi; +use ratatui::{ + layout::{Constraint, Layout, Rect}, + style::Stylize, + text::Text, + widgets::Paragraph, + Frame, +}; +use root_folders::RootFoldersUi; +use system::SystemUi; + +use crate::{ + app::App, + logos::SONARR_LOGO, + models::{ + servarr_data::sonarr::sonarr_data::SonarrData, + servarr_models::{DiskSpace, RootFolder}, + sonarr_models::DownloadRecord, + Route, + }, + utils::convert_to_gb, +}; + +use super::{ + draw_tabs, + styles::ManagarrStyle, + utils::{ + borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block, + }, + widgets::loading_block::LoadingBlock, + DrawUi, +}; + +mod blocklist; +mod downloads; +mod history; +mod indexers; +mod library; +mod root_folders; +mod sonarr_ui_utils; +mod system; + +#[cfg(test)] +#[path = "sonarr_ui_tests.rs"] +mod sonarr_ui_tests; + +pub(super) struct SonarrUi; + +impl DrawUi for SonarrUi { + fn accepts(route: Route) -> bool { + matches!(route, Route::Sonarr(_, _)) + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let content_area = draw_tabs(f, area, "Series", &app.data.sonarr_data.main_tabs); + let route = app.get_current_route(); + + match route { + _ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area), + _ if DownloadsUi::accepts(route) => DownloadsUi::draw(f, app, content_area), + _ if BlocklistUi::accepts(route) => BlocklistUi::draw(f, app, content_area), + _ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area), + _ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area), + _ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area), + _ if SystemUi::accepts(route) => SystemUi::draw(f, app, content_area), + _ => (), + } + } + + fn draw_context_row(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let [main_area, logo_area] = + Layout::horizontal([Constraint::Fill(0), Constraint::Length(20)]).areas(area); + + let [stats_area, downloads_area] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(main_area); + + draw_stats_context(f, app, stats_area); + draw_downloads_context(f, app, downloads_area); + draw_sonarr_logo(f, logo_area); + } +} + +fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let block = title_block("Stats"); + + if !app.data.sonarr_data.version.is_empty() { + f.render_widget(block, area); + let SonarrData { + root_folders, + disk_space_vec, + start_time, + .. + } = &app.data.sonarr_data; + + let mut constraints = vec![ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ]; + + constraints.append( + &mut iter::repeat(Constraint::Length(1)) + .take(disk_space_vec.len() + root_folders.items.len() + 1) + .collect(), + ); + + let stat_item_areas = Layout::vertical(constraints).margin(1).split(area); + + let version_paragraph = Paragraph::new(Text::from(format!( + "Sonarr Version: {}", + app.data.sonarr_data.version + ))) + .block(borderless_block()) + .bold(); + + let uptime = Utc::now() - start_time.to_owned(); + let days = uptime.num_days(); + let day_difference = uptime - Duration::days(days); + let hours = day_difference.num_hours(); + let hour_difference = day_difference - Duration::hours(hours); + let minutes = hour_difference.num_minutes(); + let seconds = (hour_difference - Duration::minutes(minutes)).num_seconds(); + + let uptime_paragraph = Paragraph::new(Text::from(format!( + "Uptime: {days}d {hours:0width$}:{minutes:0width$}:{seconds:0width$}", + width = 2 + ))) + .block(borderless_block()) + .bold(); + + let storage = Paragraph::new(Text::from("Storage:")).block(borderless_block().bold()); + let folders = Paragraph::new(Text::from("Root Folders:")).block(borderless_block().bold()); + + f.render_widget(version_paragraph, stat_item_areas[0]); + f.render_widget(uptime_paragraph, stat_item_areas[1]); + f.render_widget(storage, stat_item_areas[2]); + + for i in 0..disk_space_vec.len() { + let DiskSpace { + free_space, + total_space, + } = &disk_space_vec[i]; + let title = format!("Disk {}", i + 1); + let ratio = if *total_space == 0 { + 0f64 + } else { + 1f64 - (*free_space as f64 / *total_space as f64) + }; + + let space_gauge = line_gauge_with_label(title.as_str(), ratio); + + f.render_widget(space_gauge, stat_item_areas[i + 3]); + } + + f.render_widget(folders, stat_item_areas[disk_space_vec.len() + 3]); + + for i in 0..root_folders.items.len() { + let RootFolder { + path, free_space, .. + } = &root_folders.items[i]; + let space: f64 = convert_to_gb(*free_space); + let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free")) + .block(borderless_block()) + .default(); + + f.render_widget( + root_folder_space, + stat_item_areas[i + disk_space_vec.len() + 4], + ) + } + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} + +fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { + let block = title_block("Downloads"); + let downloads_vec = &app.data.sonarr_data.downloads.items; + + if !downloads_vec.is_empty() { + f.render_widget(block, area); + + let max_items = ((((area.height as f64 / 2.0).floor() * 2.0) as i64) / 2) - 1; + let items = cmp::min(downloads_vec.len(), max_items.unsigned_abs() as usize); + let download_item_areas = Layout::vertical( + iter::repeat(Constraint::Length(2)) + .take(items) + .collect::>(), + ) + .margin(1) + .split(area); + + for i in 0..items { + let DownloadRecord { + title, + sizeleft, + size, + .. + } = &downloads_vec[i]; + let percent = if *size == 0.0 { + 0.0 + } else { + 1f64 - (*sizeleft / *size) + }; + let download_gauge = line_gauge_with_title(title, percent); + + f.render_widget(download_gauge, download_item_areas[i]); + } + } else { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + } +} + +fn draw_sonarr_logo(f: &mut Frame<'_>, area: Rect) { + let logo_text = Text::from(SONARR_LOGO); + let logo = Paragraph::new(logo_text) + .light_cyan() + .block(layout_block().default()) + .centered(); + f.render_widget(logo, area); +} diff --git a/src/ui/sonarr_ui/root_folders/mod.rs b/src/ui/sonarr_ui/root_folders/mod.rs new file mode 100644 index 0000000..a8f7495 --- /dev/null +++ b/src/ui/sonarr_ui/root_folders/mod.rs @@ -0,0 +1,115 @@ +use ratatui::layout::{Constraint, Rect}; +use ratatui::widgets::{Cell, Row}; +use ratatui::Frame; + +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; +use crate::models::servarr_models::RootFolder; +use crate::models::Route; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{draw_input_box_popup, draw_popup, DrawUi}; +use crate::utils::convert_to_gb; + +#[cfg(test)] +#[path = "root_folders_ui_tests.rs"] +mod root_folders_ui_tests; + +pub(super) struct RootFoldersUi; + +impl DrawUi for RootFoldersUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return ROOT_FOLDERS_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + draw_root_folders(f, app, area); + + match active_sonarr_block { + ActiveSonarrBlock::AddRootFolderPrompt => { + draw_popup(f, app, draw_add_root_folder_prompt_box, Size::InputBox) + } + ActiveSonarrBlock::DeleteRootFolderPrompt => { + let prompt = format!( + "Do you really want to delete this root folder: \n{}?", + app.data.sonarr_data.root_folders.current_selection().path + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Root Folder") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + } + } + } +} + +fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let help_footer = app + .data + .sonarr_data + .main_tabs + .get_active_tab_contextual_help(); + let root_folders_row_mapping = |root_folders: &RootFolder| { + let RootFolder { + path, + free_space, + unmapped_folders, + .. + } = root_folders; + + let space: f64 = convert_to_gb(*free_space); + + Row::new(vec![ + Cell::from(path.to_owned()), + Cell::from(format!("{space:.2} GB")), + Cell::from( + unmapped_folders + .as_ref() + .unwrap_or(&Vec::new()) + .len() + .to_string(), + ), + ]) + .primary() + }; + + let root_folders_table = ManagarrTable::new( + Some(&mut app.data.sonarr_data.root_folders), + root_folders_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(help_footer) + .headers(["Path", "Free Space", "Unmapped Folders"]) + .constraints([ + Constraint::Ratio(3, 5), + Constraint::Ratio(1, 5), + Constraint::Ratio(1, 5), + ]); + + f.render_widget(root_folders_table, area); +} + +fn draw_add_root_folder_prompt_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + draw_input_box_popup( + f, + area, + "Add Root Folder", + app.data.sonarr_data.edit_root_folder.as_ref().unwrap(), + ); +} diff --git a/src/ui/sonarr_ui/root_folders/root_folders_ui_tests.rs b/src/ui/sonarr_ui/root_folders/root_folders_ui_tests.rs new file mode 100644 index 0000000..1e9e6c0 --- /dev/null +++ b/src/ui/sonarr_ui/root_folders/root_folders_ui_tests.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; + use crate::ui::sonarr_ui::root_folders::RootFoldersUi; + use crate::ui::DrawUi; + + #[test] + fn test_root_folders_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if ROOT_FOLDERS_BLOCKS.contains(&active_sonarr_block) { + assert!(RootFoldersUi::accepts(active_sonarr_block.into())); + } else { + assert!(!RootFoldersUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/sonarr_ui_tests.rs b/src/ui/sonarr_ui/sonarr_ui_tests.rs new file mode 100644 index 0000000..6a1630e --- /dev/null +++ b/src/ui/sonarr_ui/sonarr_ui_tests.rs @@ -0,0 +1,16 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::{ + models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock, + ui::{sonarr_ui::SonarrUi, DrawUi}, + }; + + #[test] + fn test_sonarr_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + assert!(SonarrUi::accepts(active_sonarr_block.into())); + }); + } +} diff --git a/src/ui/sonarr_ui/sonarr_ui_utils.rs b/src/ui/sonarr_ui/sonarr_ui_utils.rs new file mode 100644 index 0000000..8fff51a --- /dev/null +++ b/src/ui/sonarr_ui/sonarr_ui_utils.rs @@ -0,0 +1,183 @@ +use ratatui::style::Stylize; +use ratatui::text::Line; + +use crate::models::sonarr_models::{SonarrHistoryData, SonarrHistoryItem}; +use crate::ui::styles::ManagarrStyle; + +#[cfg(test)] +#[path = "sonarr_ui_utils_tests.rs"] +mod sonarr_ui_utils_tests; + +pub(super) fn create_grabbed_history_event_details( + history_item: SonarrHistoryItem, +) -> Vec> { + let SonarrHistoryItem { + source_title, data, .. + } = history_item; + let SonarrHistoryData { + indexer, + release_group, + series_match_type, + nzb_info_url, + download_client_name, + age, + published_date, + .. + } = data; + + vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Indexer: ".bold().secondary(), + indexer.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Release Group: ".bold().secondary(), + release_group.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Series Match Type: ".bold().secondary(), + series_match_type.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "NZB Info URL: ".bold().secondary(), + nzb_info_url.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Download Client Name: ".bold().secondary(), + download_client_name.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Age: ".bold().secondary(), + format!("{} days", age.unwrap_or("0".to_owned())).secondary(), + ]), + Line::from(vec![ + "Published Date: ".bold().secondary(), + published_date.unwrap_or_default().to_string().secondary(), + ]), + ] +} + +pub(super) fn create_download_folder_imported_history_event_details( + history_item: SonarrHistoryItem, +) -> Vec> { + let SonarrHistoryItem { + source_title, data, .. + } = history_item; + let SonarrHistoryData { + dropped_path, + imported_path, + .. + } = data; + + vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Dropped Path: ".bold().secondary(), + dropped_path.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Imported Path: ".bold().secondary(), + imported_path.unwrap_or_default().secondary(), + ]), + ] +} + +pub(super) fn create_download_failed_history_event_details( + history_item: SonarrHistoryItem, +) -> Vec> { + let SonarrHistoryItem { + source_title, data, .. + } = history_item; + let SonarrHistoryData { message, .. } = data; + + vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Message: ".bold().secondary(), + message.unwrap_or_default().secondary(), + ]), + ] +} + +pub(super) fn create_episode_file_deleted_history_event_details( + history_item: SonarrHistoryItem, +) -> Vec> { + let SonarrHistoryItem { + source_title, data, .. + } = history_item; + let SonarrHistoryData { reason, .. } = data; + + vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Reason: ".bold().secondary(), + reason.unwrap_or_default().secondary(), + ]), + ] +} + +pub(super) fn create_episode_file_renamed_history_event_details( + history_item: SonarrHistoryItem, +) -> Vec> { + let SonarrHistoryItem { + source_title, data, .. + } = history_item; + let SonarrHistoryData { + source_path, + source_relative_path, + path, + relative_path, + .. + } = data; + + vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Source Path: ".bold().secondary(), + source_path.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Source Relative Path: ".bold().secondary(), + source_relative_path.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Destination Path: ".bold().secondary(), + path.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Destination Relative Path: ".bold().secondary(), + relative_path.unwrap_or_default().secondary(), + ]), + ] +} + +pub(super) fn create_no_data_history_event_details( + history_item: SonarrHistoryItem, +) -> Vec> { + let SonarrHistoryItem { source_title, .. } = history_item; + + vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![String::new().secondary()]), + Line::from(vec!["No additional data available".bold().secondary()]), + ] +} diff --git a/src/ui/sonarr_ui/sonarr_ui_utils_tests.rs b/src/ui/sonarr_ui/sonarr_ui_utils_tests.rs new file mode 100644 index 0000000..a0255c2 --- /dev/null +++ b/src/ui/sonarr_ui/sonarr_ui_utils_tests.rs @@ -0,0 +1,240 @@ +#[cfg(test)] +mod tests { + use chrono::Utc; + use ratatui::{style::Stylize, text::Line}; + + use crate::{ + models::sonarr_models::{SonarrHistoryData, SonarrHistoryItem}, + ui::{ + sonarr_ui::sonarr_ui_utils::{ + create_download_failed_history_event_details, + create_download_folder_imported_history_event_details, + create_episode_file_deleted_history_event_details, + create_episode_file_renamed_history_event_details, create_grabbed_history_event_details, + create_no_data_history_event_details, + }, + styles::ManagarrStyle, + }, + }; + use pretty_assertions::assert_eq; + + #[test] + fn test_create_grabbed_history_event_details() { + let history_item = sonarr_history_item(); + let SonarrHistoryItem { + source_title, data, .. + } = history_item.clone(); + let SonarrHistoryData { + indexer, + release_group, + series_match_type, + nzb_info_url, + download_client_name, + age, + published_date, + .. + } = data; + let expected_vec = vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Indexer: ".bold().secondary(), + indexer.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Release Group: ".bold().secondary(), + release_group.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Series Match Type: ".bold().secondary(), + series_match_type.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "NZB Info URL: ".bold().secondary(), + nzb_info_url.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Download Client Name: ".bold().secondary(), + download_client_name.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Age: ".bold().secondary(), + format!("{} days", age.unwrap_or("0".to_owned())).secondary(), + ]), + Line::from(vec![ + "Published Date: ".bold().secondary(), + published_date.unwrap_or_default().to_string().secondary(), + ]), + ]; + + let history_details_vec = create_grabbed_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_download_folder_imported_history_event_details() { + let history_item = sonarr_history_item(); + let SonarrHistoryItem { + source_title, data, .. + } = history_item.clone(); + let SonarrHistoryData { + dropped_path, + imported_path, + .. + } = data; + let expected_vec = vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Dropped Path: ".bold().secondary(), + dropped_path.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Imported Path: ".bold().secondary(), + imported_path.unwrap_or_default().secondary(), + ]), + ]; + + let history_details_vec = create_download_folder_imported_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_download_failed_history_event_details() { + let history_item = sonarr_history_item(); + let SonarrHistoryItem { + source_title, data, .. + } = history_item.clone(); + let SonarrHistoryData { message, .. } = data; + let expected_vec = vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Message: ".bold().secondary(), + message.unwrap_or_default().secondary(), + ]), + ]; + + let history_details_vec = create_download_failed_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_episode_file_deleted_history_event_details() { + let history_item = sonarr_history_item(); + let SonarrHistoryItem { + source_title, data, .. + } = history_item.clone(); + let SonarrHistoryData { reason, .. } = data; + let expected_vec = vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Reason: ".bold().secondary(), + reason.unwrap_or_default().secondary(), + ]), + ]; + + let history_details_vec = create_episode_file_deleted_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_episode_file_renamed_history_event_details() { + let history_item = sonarr_history_item(); + let SonarrHistoryItem { + source_title, data, .. + } = history_item.clone(); + let SonarrHistoryData { + source_path, + source_relative_path, + path, + relative_path, + .. + } = data; + let expected_vec = vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![ + "Source Path: ".bold().secondary(), + source_path.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Source Relative Path: ".bold().secondary(), + source_relative_path.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Destination Path: ".bold().secondary(), + path.unwrap_or_default().secondary(), + ]), + Line::from(vec![ + "Destination Relative Path: ".bold().secondary(), + relative_path.unwrap_or_default().secondary(), + ]), + ]; + + let history_details_vec = create_episode_file_renamed_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + #[test] + fn test_create_no_data_history_event_details() { + let history_item = sonarr_history_item(); + let SonarrHistoryItem { source_title, .. } = history_item.clone(); + let expected_vec = vec![ + Line::from(vec![ + "Source Title: ".bold().secondary(), + source_title.text.secondary(), + ]), + Line::from(vec![String::new().secondary()]), + Line::from(vec!["No additional data available".bold().secondary()]), + ]; + + let history_details_vec = create_no_data_history_event_details(history_item); + + assert_eq!(expected_vec, history_details_vec); + } + + fn sonarr_history_item() -> SonarrHistoryItem { + SonarrHistoryItem { + source_title: "test.source.title".into(), + data: sonarr_history_data(), + ..SonarrHistoryItem::default() + } + } + + fn sonarr_history_data() -> SonarrHistoryData { + SonarrHistoryData { + dropped_path: Some("/dropped/test".into()), + imported_path: Some("/imported/test".into()), + indexer: Some("Test Indexer".into()), + release_group: Some("test release group".into()), + series_match_type: Some("test match type".into()), + nzb_info_url: Some("test url".into()), + download_client_name: Some("test download client".into()), + age: Some("1".into()), + published_date: Some(Utc::now()), + message: Some("test message".into()), + reason: Some("test reason".into()), + source_path: Some("/source/path".into()), + source_relative_path: Some("/relative/source/path".into()), + path: Some("/path".into()), + relative_path: Some("/relative/path".into()), + } + } +} diff --git a/src/ui/sonarr_ui/system/mod.rs b/src/ui/sonarr_ui/system/mod.rs new file mode 100644 index 0000000..f6c3824 --- /dev/null +++ b/src/ui/sonarr_ui/system/mod.rs @@ -0,0 +1,229 @@ +use std::ops::Sub; + +use chrono::Utc; +use ratatui::layout::Layout; +use ratatui::style::Style; +use ratatui::text::{Span, Text}; +use ratatui::widgets::{Cell, Paragraph, Row}; +use ratatui::{ + layout::{Constraint, Rect}, + widgets::ListItem, + Frame, +}; + +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; +use crate::models::servarr_models::QueueEvent; +use crate::models::sonarr_models::SonarrTask; +use crate::ui::sonarr_ui::system::system_details_ui::SystemDetailsUi; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{ + convert_to_minutes_hours_days, layout_block_top_border, style_log_list_item, +}; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::{ + models::Route, + ui::{utils::title_block, DrawUi}, +}; + +mod system_details_ui; + +#[cfg(test)] +#[path = "system_ui_tests.rs"] +mod system_ui_tests; + +pub(super) const TASK_TABLE_HEADERS: [&str; 4] = + ["Name", "Interval", "Last Execution", "Next Execution"]; + +pub(super) const TASK_TABLE_CONSTRAINTS: [Constraint; 4] = [ + Constraint::Percentage(30), + Constraint::Percentage(23), + Constraint::Percentage(23), + Constraint::Percentage(23), +]; + +pub(super) struct SystemUi; + +impl DrawUi for SystemUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return SystemDetailsUi::accepts(route) || active_sonarr_block == ActiveSonarrBlock::System; + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let route = app.get_current_route(); + draw_system_ui_layout(f, app, area); + + if SystemDetailsUi::accepts(route) { + SystemDetailsUi::draw(f, app, area); + } + } +} + +fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let [activities_area, logs_area, help_area] = Layout::vertical([ + Constraint::Ratio(1, 2), + Constraint::Ratio(1, 2), + Constraint::Min(2), + ]) + .areas(area); + + let [tasks_area, events_area] = + Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(activities_area); + + draw_tasks(f, app, tasks_area); + draw_queued_events(f, app, events_area); + draw_logs(f, app, logs_area); + draw_help(f, app, help_area); +} + +fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let tasks_row_mapping = |task: &SonarrTask| { + let task_props = extract_task_props(task); + + Row::new(vec![ + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.next_execution), + ]) + .primary() + }; + let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping) + .block(title_block("Tasks")) + .loading(app.is_loading) + .highlight_rows(false) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); + + f.render_widget(tasks_table, area); +} + +pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let events_row_mapping = |event: &QueueEvent| { + let queued = convert_to_minutes_hours_days(Utc::now().sub(event.queued).num_minutes()); + let queued_string = if queued != "now" { + format!("{queued} ago") + } else { + queued + }; + let started_string = if event.started.is_some() { + let started = + convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes()); + + if started != "now" { + format!("{started} ago") + } else { + started + } + } else { + String::new() + }; + + let duration = if event.duration.is_some() { + &event.duration.as_ref().unwrap()[..8] + } else { + "" + }; + + Row::new(vec![ + Cell::from(event.trigger.clone()), + Cell::from(event.status.clone()), + Cell::from(event.command_name.clone()), + Cell::from(queued_string), + Cell::from(started_string), + Cell::from(duration.to_owned()), + ]) + .primary() + }; + let events_table = ManagarrTable::new( + Some(&mut app.data.sonarr_data.queued_events), + events_row_mapping, + ) + .block(title_block("Queued Events")) + .loading(app.is_loading) + .highlight_rows(false) + .headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"]) + .constraints([ + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(30), + Constraint::Percentage(16), + Constraint::Percentage(14), + Constraint::Percentage(14), + ]); + + f.render_widget(events_table, area); +} + +fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let block = title_block("Logs"); + + if app.data.sonarr_data.logs.items.is_empty() { + f.render_widget(LoadingBlock::new(app.is_loading, block), area); + return; + } + + let logs_box = SelectableList::new(&mut app.data.sonarr_data.logs, |log| { + let log_line = log.to_string(); + let level = log_line.split('|').collect::>()[1].to_string(); + + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) + }) + .block(block) + .highlight_style(Style::new().default()); + + f.render_widget(logs_box, area); +} + +fn draw_help(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let help_text = Text::from( + format!( + " {}", + app + .data + .sonarr_data + .main_tabs + .get_active_tab_contextual_help() + .unwrap() + ) + .help(), + ); + let help_paragraph = Paragraph::new(help_text) + .block(layout_block_top_border()) + .left_aligned(); + + f.render_widget(help_paragraph, area); +} + +pub(super) struct TaskProps { + pub(super) name: String, + pub(super) interval: String, + pub(super) last_execution: String, + pub(super) next_execution: String, +} + +pub(super) fn extract_task_props(task: &SonarrTask) -> TaskProps { + let interval = convert_to_minutes_hours_days(task.interval); + let next_execution = + convert_to_minutes_hours_days((task.next_execution - Utc::now()).num_minutes()); + let last_execution = + convert_to_minutes_hours_days((Utc::now() - task.last_execution).num_minutes()); + let last_execution_string = if last_execution != "now" { + format!("{last_execution} ago") + } else { + last_execution + }; + + TaskProps { + name: task.name.clone(), + interval, + last_execution: last_execution_string, + next_execution, + } +} diff --git a/src/ui/sonarr_ui/system/system_details_ui.rs b/src/ui/sonarr_ui/system/system_details_ui.rs new file mode 100644 index 0000000..72eafe2 --- /dev/null +++ b/src/ui/sonarr_ui/system/system_details_ui.rs @@ -0,0 +1,165 @@ +use ratatui::layout::{Alignment, Rect}; +use ratatui::text::{Span, Text}; +use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; +use ratatui::Frame; + +use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES}; +use crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES; +use crate::app::App; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS}; +use crate::models::sonarr_models::SonarrTask; +use crate::models::Route; +use crate::ui::sonarr_ui::system::{ + draw_queued_events, extract_task_props, TASK_TABLE_CONSTRAINTS, TASK_TABLE_HEADERS, +}; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{borderless_block, style_log_list_item, title_block}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::loading_block::LoadingBlock; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::widgets::selectable_list::SelectableList; +use crate::ui::{draw_popup, DrawUi}; + +#[cfg(test)] +#[path = "system_details_ui_tests.rs"] +mod system_details_ui_tests; + +pub(super) struct SystemDetailsUi; + +impl DrawUi for SystemDetailsUi { + fn accepts(route: Route) -> bool { + if let Route::Sonarr(active_sonarr_block, _) = route { + return SYSTEM_DETAILS_BLOCKS.contains(&active_sonarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) { + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { + match active_sonarr_block { + ActiveSonarrBlock::SystemLogs => { + draw_logs_popup(f, app); + } + ActiveSonarrBlock::SystemTasks | ActiveSonarrBlock::SystemTaskStartConfirmPrompt => { + draw_popup(f, app, draw_tasks_popup, Size::Large) + } + ActiveSonarrBlock::SystemQueuedEvents => { + draw_popup(f, app, draw_queued_events, Size::Medium) + } + ActiveSonarrBlock::SystemUpdates => { + draw_updates_popup(f, app); + } + _ => (), + } + } + } +} + +fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let block = title_block("Log Details"); + let help_footer = format!( + "<↑↓←→> scroll | {}", + build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) + ); + + if app.data.sonarr_data.log_details.items.is_empty() { + let loading = LoadingBlock::new(app.is_loading, borderless_block()); + let popup = Popup::new(loading) + .size(Size::Large) + .block(block) + .footer(&help_footer); + + f.render_widget(popup, f.area()); + return; + } + + let logs_list = SelectableList::new(&mut app.data.sonarr_data.log_details, |log| { + let log_line = log.to_string(); + let level = log.text.split('|').collect::>()[1].to_string(); + + style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) + }) + .block(borderless_block()); + let popup = Popup::new(logs_list) + .size(Size::Large) + .block(block) + .footer(&help_footer); + + f.render_widget(popup, f.area()); +} + +fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let help_footer = Some(build_context_clue_string(&SYSTEM_TASKS_CONTEXT_CLUES)); + let tasks_row_mapping = |task: &SonarrTask| { + let task_props = extract_task_props(task); + + Row::new(vec![ + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.next_execution), + ]) + .primary() + }; + let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping) + .block(borderless_block()) + .loading(app.is_loading) + .margin(1) + .footer(help_footer) + .footer_alignment(Alignment::Center) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); + + f.render_widget(title_block("Tasks"), area); + f.render_widget(tasks_table, area); + + if matches!( + app.get_current_route(), + Route::Sonarr(ActiveSonarrBlock::SystemTaskStartConfirmPrompt, _) + ) { + let prompt = format!( + "Do you want to manually start this task: {}?", + app.data.sonarr_data.tasks.current_selection().name + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Start Task") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } +} + +fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let help_footer = format!( + "<↑↓> scroll | {}", + build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) + ); + let updates = app.data.sonarr_data.updates.get_text(); + let block = title_block("Updates"); + + if !updates.is_empty() && !app.is_loading { + let updates_paragraph = Paragraph::new(Text::from(updates)) + .block(borderless_block()) + .scroll((app.data.sonarr_data.updates.offset, 0)); + let popup = Popup::new(updates_paragraph) + .size(Size::Large) + .block(block) + .footer(&help_footer); + + f.render_widget(popup, f.area()); + } else { + let loading = LoadingBlock::new(app.is_loading, borderless_block()); + let popup = Popup::new(loading) + .size(Size::Large) + .block(block) + .footer(&help_footer); + + f.render_widget(popup, f.area()); + } +} diff --git a/src/ui/sonarr_ui/system/system_details_ui_tests.rs b/src/ui/sonarr_ui/system/system_details_ui_tests.rs new file mode 100644 index 0000000..80dc239 --- /dev/null +++ b/src/ui/sonarr_ui/system/system_details_ui_tests.rs @@ -0,0 +1,21 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::ui::sonarr_ui::system::system_details_ui::SystemDetailsUi; + use crate::ui::DrawUi; + + #[test] + fn test_system_details_ui_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if SYSTEM_DETAILS_BLOCKS.contains(&active_sonarr_block) { + assert!(SystemDetailsUi::accepts(active_sonarr_block.into())); + } else { + assert!(!SystemDetailsUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/sonarr_ui/system/system_ui_tests.rs b/src/ui/sonarr_ui/system/system_ui_tests.rs new file mode 100644 index 0000000..dc43bdc --- /dev/null +++ b/src/ui/sonarr_ui/system/system_ui_tests.rs @@ -0,0 +1,25 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS, + }; + use crate::ui::sonarr_ui::system::SystemUi; + use crate::ui::DrawUi; + + #[test] + fn test_system_ui_accepts() { + let mut system_ui_blocks = Vec::new(); + system_ui_blocks.push(ActiveSonarrBlock::System); + system_ui_blocks.extend(SYSTEM_DETAILS_BLOCKS); + + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if system_ui_blocks.contains(&active_sonarr_block) { + assert!(SystemUi::accepts(active_sonarr_block.into())); + } else { + assert!(!SystemUi::accepts(active_sonarr_block.into())); + } + }); + } +} diff --git a/src/ui/styles.rs b/src/ui/styles.rs index 744f633..6d5e055 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -14,6 +14,7 @@ where #[allow(clippy::new_ret_no_self)] fn new() -> T; fn awaiting_import(self) -> T; + fn indeterminate(self) -> T; fn default(self) -> T; fn downloaded(self) -> T; fn downloading(self) -> T; @@ -44,6 +45,10 @@ where self.fg(COLOR_ORANGE) } + fn indeterminate(self) -> T { + self.fg(COLOR_ORANGE) + } + fn default(self) -> T { self.white() } diff --git a/src/ui/styles_tests.rs b/src/ui/styles_tests.rs index 55169f7..98259ec 100644 --- a/src/ui/styles_tests.rs +++ b/src/ui/styles_tests.rs @@ -18,6 +18,11 @@ mod test { ); } + #[test] + fn test_style_indeterminate() { + assert_eq!(Style::new().indeterminate(), Style::new().fg(COLOR_ORANGE)); + } + #[test] fn test_style_default() { assert_eq!(Style::new().default(), Style::new().white()); diff --git a/src/ui/utils.rs b/src/ui/utils.rs index bf1e24b..3a7aba9 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -3,7 +3,7 @@ use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Color, Style, Stylize}; use ratatui::symbols; use ratatui::text::{Line, Span, Text}; -use ratatui::widgets::{Block, BorderType, Borders, LineGauge, Paragraph, Wrap}; +use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap}; pub const COLOR_TEAL: Color = Color::Rgb(35, 50, 55); @@ -116,3 +116,51 @@ pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> { pub fn get_width_from_percentage(area: Rect, percentage: u16) -> usize { (area.width as f64 * (percentage as f64 / 100.0)) as usize } + +pub(super) fn style_log_list_item(list_item: ListItem<'_>, level: String) -> ListItem<'_> { + match level.to_lowercase().as_str() { + "trace" => list_item.gray(), + "debug" => list_item.blue(), + "info" => list_item.style(Style::new().default()), + "warn" => list_item.style(Style::new().secondary()), + "error" => list_item.style(Style::new().failure()), + "fatal" => list_item.style(Style::new().failure().bold()), + _ => list_item.style(Style::new().default()), + } +} + +pub(super) fn convert_to_minutes_hours_days(time: i64) -> String { + if time < 60 { + if time == 0 { + "now".to_owned() + } else if time == 1 { + format!("{time} minute") + } else { + format!("{time} minutes") + } + } else if time / 60 < 24 { + let hours = time / 60; + if hours == 1 { + format!("{hours} hour") + } else { + format!("{hours} hours") + } + } else { + let days = time / (60 * 24); + if days == 1 { + format!("{days} day") + } else { + format!("{days} days") + } + } +} + +pub(super) fn decorate_peer_style(seeders: u64, leechers: u64, text: Text<'_>) -> Text<'_> { + if seeders == 0 { + text.failure() + } else if seeders < leechers { + text.warning() + } else { + text.success() + } +} diff --git a/src/ui/utils_tests.rs b/src/ui/utils_tests.rs index 888e6bd..dfb288a 100644 --- a/src/ui/utils_tests.rs +++ b/src/ui/utils_tests.rs @@ -1,17 +1,17 @@ #[cfg(test)] mod test { - use pretty_assertions::assert_eq; - use ratatui::layout::{Alignment, Rect}; - use ratatui::style::{Color, Modifier, Style}; - use ratatui::text::Span; - use ratatui::widgets::{Block, BorderType, Borders}; - use crate::ui::utils::{ - borderless_block, centered_rect, get_width_from_percentage, layout_block, - layout_block_bottom_border, layout_block_top_border, layout_block_top_border_with_title, - layout_block_with_title, logo_block, style_block_highlight, title_block, title_block_centered, - title_style, + borderless_block, centered_rect, convert_to_minutes_hours_days, decorate_peer_style, + get_width_from_percentage, layout_block, layout_block_bottom_border, layout_block_top_border, + layout_block_top_border_with_title, layout_block_with_title, logo_block, style_block_highlight, + style_log_list_item, title_block, title_block_centered, title_style, }; + use pretty_assertions::{assert_eq, assert_str_eq}; + use ratatui::layout::{Alignment, Rect}; + use ratatui::style::{Color, Modifier, Style, Stylize}; + use ratatui::text::{Span, Text}; + use ratatui::widgets::{Block, BorderType, Borders, ListItem}; + use rstest::rstest; #[test] fn test_layout_block() { @@ -174,6 +174,103 @@ mod test { ); } + #[test] + fn test_determine_log_style_by_level() { + use crate::ui::styles::ManagarrStyle; + let list_item = ListItem::new(Text::from(Span::raw("test"))); + + assert_eq!( + style_log_list_item(list_item.clone(), "trace".to_string()), + list_item.clone().gray() + ); + assert_eq!( + style_log_list_item(list_item.clone(), "debug".to_string()), + list_item.clone().blue() + ); + assert_eq!( + style_log_list_item(list_item.clone(), "info".to_string()), + list_item.clone().style(Style::new().default()) + ); + assert_eq!( + style_log_list_item(list_item.clone(), "warn".to_string()), + list_item.clone().style(Style::new().secondary()) + ); + assert_eq!( + style_log_list_item(list_item.clone(), "error".to_string()), + list_item.clone().style(Style::new().failure()) + ); + assert_eq!( + style_log_list_item(list_item.clone(), "fatal".to_string()), + list_item.clone().style(Style::new().failure().bold()) + ); + assert_eq!( + style_log_list_item(list_item.clone(), "".to_string()), + list_item.style(Style::new().default()) + ); + } + + #[test] + fn test_determine_log_style_by_level_case_insensitive() { + let list_item = ListItem::new(Text::from(Span::raw("test"))); + + assert_eq!( + style_log_list_item(list_item.clone(), "TrAcE".to_string()), + list_item.gray() + ); + } + + #[test] + fn test_convert_to_minutes_hours_days_minutes() { + assert_str_eq!(convert_to_minutes_hours_days(0), "now"); + assert_str_eq!(convert_to_minutes_hours_days(1), "1 minute"); + assert_str_eq!(convert_to_minutes_hours_days(2), "2 minutes"); + } + + #[test] + fn test_convert_to_minutes_hours_days_hours() { + assert_str_eq!(convert_to_minutes_hours_days(60), "1 hour"); + assert_str_eq!(convert_to_minutes_hours_days(120), "2 hours"); + } + + #[test] + fn test_convert_to_minutes_hours_days_days() { + assert_str_eq!(convert_to_minutes_hours_days(1440), "1 day"); + assert_str_eq!(convert_to_minutes_hours_days(2880), "2 days"); + } + + #[rstest] + #[case(0, 0, PeerStyle::Failure)] + #[case(1, 2, PeerStyle::Warning)] + #[case(4, 2, PeerStyle::Success)] + fn test_decorate_peer_style( + #[case] seeders: u64, + #[case] leechers: u64, + #[case] expected_style: PeerStyle, + ) { + use crate::ui::styles::ManagarrStyle; + let text = Text::from("test"); + match expected_style { + PeerStyle::Failure => assert_eq!( + decorate_peer_style(seeders, leechers, text.clone()), + text.failure() + ), + PeerStyle::Warning => assert_eq!( + decorate_peer_style(seeders, leechers, text.clone()), + text.warning() + ), + PeerStyle::Success => assert_eq!( + decorate_peer_style(seeders, leechers, text.clone()), + text.success() + ), + } + } + + enum PeerStyle { + Failure, + Warning, + Success, + } + fn rect() -> Rect { Rect { x: 0, diff --git a/src/ui/widgets/button.rs b/src/ui/widgets/button.rs index 26217b9..2c3524d 100644 --- a/src/ui/widgets/button.rs +++ b/src/ui/widgets/button.rs @@ -1,50 +1,26 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_block, style_block_highlight}; +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::prelude::{Style, Text, Widget}; use ratatui::style::Styled; use ratatui::widgets::Paragraph; -#[cfg(test)] -#[path = "button_tests.rs"] -mod button_tests; - -#[derive(Default)] +#[derive(Default, Setters)] pub struct Button<'a> { title: &'a str, + #[setters(strip_option)] label: Option<&'a str>, + #[setters(strip_option)] icon: Option<&'a str>, + #[setters(into)] style: Style, + #[setters(rename = "selected")] is_selected: bool, } impl<'a> Button<'a> { - pub fn title(mut self, title: &'a str) -> Button<'a> { - self.title = title; - self - } - - pub fn label(mut self, label: &'a str) -> Button<'a> { - self.label = Some(label); - self - } - - pub fn icon(mut self, icon: &'a str) -> Button<'a> { - self.icon = Some(icon); - self - } - - pub fn style>(mut self, style: S) -> Button<'a> { - self.style = style.into(); - self - } - - pub fn selected(mut self, is_selected: bool) -> Button<'a> { - self.is_selected = is_selected; - self - } - fn render_button_with_icon(self, area: Rect, buf: &mut Buffer) { let [title_area, icon_area] = Layout::horizontal([ Constraint::Length(self.title.len() as u16), diff --git a/src/ui/widgets/button_tests.rs b/src/ui/widgets/button_tests.rs deleted file mode 100644 index 7273a76..0000000 --- a/src/ui/widgets/button_tests.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::ui::widgets::button::Button; - use pretty_assertions::{assert_eq, assert_str_eq}; - use ratatui::style::{Style, Stylize}; - - #[test] - fn test_title() { - let button = Button::default().title("Title"); - - assert_str_eq!(button.title, "Title"); - } - - #[test] - fn test_label() { - let button = Button::default().label("Label"); - - assert_eq!(button.label, Some("Label")); - } - - #[test] - fn test_icon() { - let button = Button::default().icon("Icon"); - - assert_eq!(button.icon, Some("Icon")); - } - - #[test] - fn test_style() { - let button = Button::default().style(Style::new().bold()); - - assert_eq!(button.style, Style::new().bold()); - } - - #[test] - fn test_selected() { - let button = Button::default().selected(true); - - assert!(button.is_selected); - } -} diff --git a/src/ui/widgets/checkbox.rs b/src/ui/widgets/checkbox.rs index 08ee9d2..a3b4f81 100644 --- a/src/ui/widgets/checkbox.rs +++ b/src/ui/widgets/checkbox.rs @@ -1,19 +1,19 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{borderless_block, layout_block, style_block_highlight}; +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::prelude::Text; use ratatui::style::Stylize; use ratatui::widgets::{Paragraph, Widget}; -#[cfg(test)] -#[path = "checkbox_tests.rs"] -mod checkbox_tests; - -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(PartialEq, Debug, Copy, Clone, Setters)] pub struct Checkbox<'a> { + #[setters(skip)] label: &'a str, + #[setters(rename = "checked")] is_checked: bool, + #[setters(rename = "highlighted")] is_highlighted: bool, } @@ -26,16 +26,6 @@ impl<'a> Checkbox<'a> { } } - pub fn checked(mut self, is_checked: bool) -> Checkbox<'a> { - self.is_checked = is_checked; - self - } - - pub fn highlighted(mut self, is_selected: bool) -> Checkbox<'a> { - self.is_highlighted = is_selected; - self - } - fn render_checkbox(self, area: Rect, buf: &mut Buffer) { let check = if self.is_checked { "✔" } else { "" }; let [label_area, checkbox_area] = diff --git a/src/ui/widgets/checkbox_tests.rs b/src/ui/widgets/checkbox_tests.rs deleted file mode 100644 index 7af1bba..0000000 --- a/src/ui/widgets/checkbox_tests.rs +++ /dev/null @@ -1,32 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::ui::widgets::checkbox::Checkbox; - use pretty_assertions::assert_str_eq; - - #[test] - fn test_checkbox_new() { - let checkbox = Checkbox::new("test"); - - assert_str_eq!(checkbox.label, "test"); - assert!(!checkbox.is_checked); - assert!(!checkbox.is_highlighted); - } - - #[test] - fn test_checkbox_checked() { - let checkbox = Checkbox::new("test").checked(true); - - assert_str_eq!(checkbox.label, "test"); - assert!(checkbox.is_checked); - assert!(!checkbox.is_highlighted); - } - - #[test] - fn test_checkbox_highlighted() { - let checkbox = Checkbox::new("test").highlighted(true); - - assert_str_eq!(checkbox.label, "test"); - assert!(!checkbox.is_checked); - assert!(checkbox.is_highlighted); - } -} diff --git a/src/ui/widgets/confirmation_prompt.rs b/src/ui/widgets/confirmation_prompt.rs index 521f638..f026b1b 100644 --- a/src/ui/widgets/confirmation_prompt.rs +++ b/src/ui/widgets/confirmation_prompt.rs @@ -1,9 +1,9 @@ -use crate::app::context_clues::build_context_clue_string; -use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES; +use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::text::Text; @@ -14,10 +14,13 @@ use std::iter; #[path = "confirmation_prompt_tests.rs"] mod confirmation_prompt_tests; +#[derive(Setters)] pub struct ConfirmationPrompt<'a> { title: &'a str, prompt: &'a str, + #[setters(strip_option)] content: Option>, + #[setters(strip_option)] checkboxes: Option>>, yes_no_value: bool, yes_no_highlighted: bool, @@ -35,36 +38,6 @@ impl<'a> ConfirmationPrompt<'a> { } } - pub fn title(mut self, title: &'a str) -> Self { - self.title = title; - self - } - - pub fn prompt(mut self, prompt: &'a str) -> Self { - self.prompt = prompt; - self - } - - pub fn content(mut self, content: Paragraph<'a>) -> Self { - self.content = Some(content); - self - } - - pub fn checkboxes(mut self, checkboxes: Vec>) -> Self { - self.checkboxes = Some(checkboxes); - self - } - - pub fn yes_no_value(mut self, yes_highlighted: bool) -> Self { - self.yes_no_value = yes_highlighted; - self - } - - pub fn yes_no_highlighted(mut self, yes_highlighted: bool) -> Self { - self.yes_no_highlighted = yes_highlighted; - self - } - fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) { title_block_centered(self.title).render(area, buf); let help_text = diff --git a/src/ui/widgets/confirmation_prompt_tests.rs b/src/ui/widgets/confirmation_prompt_tests.rs index 6173ef7..61d98d2 100644 --- a/src/ui/widgets/confirmation_prompt_tests.rs +++ b/src/ui/widgets/confirmation_prompt_tests.rs @@ -1,9 +1,7 @@ #[cfg(test)] mod tests { - use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use pretty_assertions::{assert_eq, assert_str_eq}; - use ratatui::widgets::Paragraph; #[test] fn test_confirmation_prompt_new() { @@ -16,78 +14,4 @@ mod tests { assert!(!confirmation_prompt.yes_no_value); assert!(confirmation_prompt.yes_no_highlighted); } - - #[test] - fn test_confirmation_prompt_title() { - let confirmation_prompt = ConfirmationPrompt::new().title("title"); - - assert_str_eq!(confirmation_prompt.title, "title"); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.content, None); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(!confirmation_prompt.yes_no_value); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_prompt() { - let confirmation_prompt = ConfirmationPrompt::new().prompt("prompt"); - - assert_str_eq!(confirmation_prompt.prompt, "prompt"); - assert_str_eq!(confirmation_prompt.title, ""); - assert_eq!(confirmation_prompt.content, None); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(!confirmation_prompt.yes_no_value); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_content() { - let content = Paragraph::new("content"); - let confirmation_prompt = ConfirmationPrompt::new().content(content.clone()); - - assert_eq!(confirmation_prompt.content, Some(content)); - assert_str_eq!(confirmation_prompt.title, ""); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(!confirmation_prompt.yes_no_value); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_checkboxes() { - let checkboxes = vec![Checkbox::new("test").highlighted(true).checked(false)]; - let confirmation_prompt = ConfirmationPrompt::new().checkboxes(checkboxes.clone()); - - assert_eq!(confirmation_prompt.checkboxes, Some(checkboxes)); - assert_str_eq!(confirmation_prompt.title, ""); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.content, None); - assert!(!confirmation_prompt.yes_no_value); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_yes_no_value() { - let confirmation_prompt = ConfirmationPrompt::new().yes_no_value(true); - - assert!(confirmation_prompt.yes_no_value); - assert_str_eq!(confirmation_prompt.title, ""); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.content, None); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_yes_no_highlighted() { - let confirmation_prompt = ConfirmationPrompt::new().yes_no_highlighted(false); - - assert!(!confirmation_prompt.yes_no_highlighted); - assert_str_eq!(confirmation_prompt.title, ""); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.content, None); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(!confirmation_prompt.yes_no_value); - } } diff --git a/src/ui/widgets/input_box.rs b/src/ui/widgets/input_box.rs index fa0f87c..e0ec7e9 100644 --- a/src/ui/widgets/input_box.rs +++ b/src/ui/widgets/input_box.rs @@ -1,8 +1,9 @@ +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Position, Rect}; use ratatui::prelude::Text; use ratatui::style::{Style, Styled, Stylize}; -use ratatui::widgets::{Block, Paragraph, Widget}; +use ratatui::widgets::{Block, Paragraph, Widget, WidgetRef}; use ratatui::Frame; use crate::ui::styles::ManagarrStyle; @@ -12,14 +13,20 @@ use crate::ui::utils::{borderless_block, layout_block}; #[path = "input_box_tests.rs"] mod input_box_tests; +#[derive(Default, Setters)] +#[cfg_attr(test, derive(Debug, PartialEq))] pub struct InputBox<'a> { content: &'a str, offset: usize, + #[setters(into)] style: Style, block: Block<'a>, + #[setters(strip_option)] label: Option<&'a str>, cursor_after_string: bool, + #[setters(rename = "highlighted", strip_option)] is_highlighted: Option, + #[setters(rename = "selected", strip_option)] is_selected: Option, } @@ -37,41 +44,6 @@ impl<'a> InputBox<'a> { } } - pub fn style>(mut self, style: S) -> InputBox<'a> { - self.style = style.into(); - self - } - - pub fn block(mut self, block: Block<'a>) -> InputBox<'a> { - self.block = block; - self - } - - pub fn label(mut self, label: &'a str) -> InputBox<'a> { - self.label = Some(label); - self - } - - pub fn offset(mut self, offset: usize) -> InputBox<'a> { - self.offset = offset; - self - } - - pub fn cursor_after_string(mut self, cursor_after_string: bool) -> InputBox<'a> { - self.cursor_after_string = cursor_after_string; - self - } - - pub fn highlighted(mut self, is_highlighted: bool) -> InputBox<'a> { - self.is_highlighted = Some(is_highlighted); - self - } - - pub fn selected(mut self, is_selected: bool) -> InputBox<'a> { - self.is_selected = Some(is_selected); - self - } - pub fn is_selected(&self) -> bool { self.is_selected.unwrap_or_default() } @@ -96,7 +68,7 @@ impl<'a> InputBox<'a> { } } - fn render_input_box(self, area: Rect, buf: &mut Buffer) { + fn render_input_box(&self, area: Rect, buf: &mut Buffer) { let style = if matches!(self.is_highlighted, Some(true)) && matches!(self.is_selected, Some(false)) { Style::new().system_function().bold() @@ -106,7 +78,7 @@ impl<'a> InputBox<'a> { let input_box_paragraph = Paragraph::new(Text::from(self.content)) .style(style) - .block(self.block); + .block(self.block.clone()); if let Some(label) = self.label { let [label_area, text_box_area] = @@ -133,6 +105,12 @@ impl<'a> Widget for InputBox<'a> { } } +impl<'a> WidgetRef for InputBox<'a> { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + self.render_input_box(area, buf); + } +} + impl<'a> Styled for InputBox<'a> { type Item = InputBox<'a>; diff --git a/src/ui/widgets/input_box_popup.rs b/src/ui/widgets/input_box_popup.rs new file mode 100644 index 0000000..8e92a38 --- /dev/null +++ b/src/ui/widgets/input_box_popup.rs @@ -0,0 +1,60 @@ +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{background_block, borderless_block, centered_rect}; +use ratatui::buffer::Buffer; +use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::widgets::{Block, Clear, Paragraph, Widget, WidgetRef}; + +use super::input_box::InputBox; + +#[cfg(test)] +#[path = "input_box_popup_tests.rs"] +mod input_box_popup_tests; + +pub struct InputBoxPopup<'a> { + input_box: InputBox<'a>, +} + +impl<'a> InputBoxPopup<'a> { + pub fn new(content: &'a str) -> Self { + Self { + input_box: InputBox::new(content), + } + } + + pub fn block(mut self, block: Block<'a>) -> InputBoxPopup<'a> { + self.input_box = self.input_box.block(block); + self + } + + pub fn offset(mut self, offset: usize) -> InputBoxPopup<'a> { + self.input_box = self.input_box.offset(offset); + self + } + + fn render_popup(&self, area: Rect, buf: &mut Buffer) { + let popup_area = Rect { + height: 6, + ..centered_rect(30, 20, area) + }; + Clear.render(popup_area, buf); + background_block().render(popup_area, buf); + + let [text_box_area, help_area] = + Layout::vertical([Constraint::Length(3), Constraint::Length(1)]) + .margin(1) + .areas(popup_area); + self.input_box.render_ref(text_box_area, buf); + + let help = Paragraph::new(" cancel") + .help() + .centered() + .block(borderless_block()); + help.render(help_area, buf); + } +} + +impl<'a> WidgetRef for InputBoxPopup<'a> { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + self.render_popup(area, buf); + } +} diff --git a/src/ui/widgets/input_box_popup_tests.rs b/src/ui/widgets/input_box_popup_tests.rs new file mode 100644 index 0000000..962a3b9 --- /dev/null +++ b/src/ui/widgets/input_box_popup_tests.rs @@ -0,0 +1,34 @@ +#[cfg(test)] +mod tests { + use crate::ui::utils::layout_block; + use crate::ui::widgets::input_box::InputBox; + use crate::ui::widgets::input_box_popup::InputBoxPopup; + use pretty_assertions::assert_eq; + + #[test] + fn test_input_box_popup_new() { + let expected_input_box = InputBox::new("test"); + + let input_box_popup = InputBoxPopup::new("test"); + + assert_eq!(input_box_popup.input_box, expected_input_box); + } + + #[test] + fn test_input_box_popup_block() { + let expected_input_box = InputBox::new("test").block(layout_block().title("title")); + + let input_box_popup = InputBoxPopup::new("test").block(layout_block().title("title")); + + assert_eq!(input_box_popup.input_box, expected_input_box); + } + + #[test] + fn test_input_box_popup_offset() { + let expected_input_box = InputBox::new("test").offset(5); + + let input_box_popup = InputBoxPopup::new("test").offset(5); + + assert_eq!(input_box_popup.input_box, expected_input_box); + } +} diff --git a/src/ui/widgets/input_box_tests.rs b/src/ui/widgets/input_box_tests.rs index d4d87b8..a26a05e 100644 --- a/src/ui/widgets/input_box_tests.rs +++ b/src/ui/widgets/input_box_tests.rs @@ -20,104 +20,6 @@ mod tests { assert_eq!(input_box.is_selected, None); } - #[test] - fn test_input_box_style() { - let input_box = InputBox::new("test").style(Style::new().highlight()); - - assert_eq!(input_box.style, Style::new().highlight()); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_block() { - let input_box = InputBox::new("test").block(layout_block().title("title")); - - assert_eq!(input_box.block, layout_block().title("title")); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_label() { - let input_box = InputBox::new("test").label("label"); - - assert_str_eq!(input_box.label.unwrap(), "label"); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_offset() { - let input_box = InputBox::new("test").offset(1); - - assert_eq!(input_box.offset, 1); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_cursor_after_string() { - let input_box = InputBox::new("test").cursor_after_string(false); - - assert!(!input_box.cursor_after_string); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_highlighted() { - let input_box = InputBox::new("test").highlighted(true); - - assert_eq!(input_box.is_highlighted, Some(true)); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_selected() { - let input_box = InputBox::new("test").selected(true); - - assert_eq!(input_box.is_selected, Some(true)); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - } - #[test] fn test_input_box_is_selected() { let input_box = InputBox::new("test").selected(true); diff --git a/src/ui/widgets/managarr_table.rs b/src/ui/widgets/managarr_table.rs index eb9b5c9..1b3db85 100644 --- a/src/ui/widgets/managarr_table.rs +++ b/src/ui/widgets/managarr_table.rs @@ -1,36 +1,58 @@ +use super::input_box_popup::InputBoxPopup; +use super::message::Message; +use super::popup::Size; use crate::models::stateful_table::StatefulTable; use crate::ui::styles::ManagarrStyle; -use crate::ui::utils::layout_block_top_border; +use crate::ui::utils::{centered_rect, layout_block_top_border, title_block_centered}; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::popup::Popup; use crate::ui::widgets::selectable_list::SelectableList; use crate::ui::HIGHLIGHT_SYMBOL; +use derive_setters::Setters; use ratatui::buffer::Buffer; -use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::layout::{Alignment, Constraint, Layout, Position, Rect}; use ratatui::prelude::{Style, Stylize, Text}; -use ratatui::widgets::{Block, ListItem, Paragraph, Row, StatefulWidget, Table, Widget}; +use ratatui::widgets::{Block, ListItem, Paragraph, Row, StatefulWidget, Table, Widget, WidgetRef}; +use ratatui::Frame; use std::fmt::Debug; +use std::sync::atomic::Ordering; #[cfg(test)] #[path = "managarr_table_tests.rs"] mod managarr_table_tests; +#[derive(Setters)] pub struct ManagarrTable<'a, T, F> where F: Fn(&T) -> Row<'a>, T: Clone + PartialEq + Eq + Debug, { + #[setters(strip_option)] content: Option<&'a mut StatefulTable>, + #[setters(skip)] table_headers: Vec, + #[setters(skip)] constraints: Vec, row_mapper: F, footer: Option, footer_alignment: Alignment, block: Block<'a>, margin: u16, + #[setters(rename = "loading")] is_loading: bool, highlight_rows: bool, + #[setters(rename = "sorting")] is_sorting: bool, + #[setters(rename = "searching")] + is_searching: bool, + search_produced_empty_results: bool, + #[setters(rename = "filtering")] + is_filtering: bool, + filter_produced_empty_results: bool, + search_box_content_length: usize, + search_box_offset: usize, + filter_box_content_length: usize, + filter_box_offset: usize, } impl<'a, T, F> ManagarrTable<'a, T, F> @@ -39,8 +61,8 @@ where T: Clone + PartialEq + Eq + Debug, { pub fn new(content: Option<&'a mut StatefulTable>, row_mapper: F) -> Self { - Self { - content, + let mut managarr_table = Self { + content: None, table_headers: Vec::new(), constraints: Vec::new(), row_mapper, @@ -51,7 +73,28 @@ where is_loading: false, highlight_rows: true, is_sorting: false, + is_searching: false, + search_produced_empty_results: false, + is_filtering: false, + filter_produced_empty_results: false, + search_box_content_length: 0, + search_box_offset: 0, + filter_box_content_length: 0, + filter_box_offset: 0, + }; + + if let Some(content) = content.as_ref() { + if let Some(search) = content.search.as_ref() { + managarr_table.search_box_content_length = search.text.len(); + managarr_table.search_box_offset = search.offset.load(Ordering::SeqCst); + } else if let Some(filter) = content.filter.as_ref() { + managarr_table.filter_box_content_length = filter.text.len(); + managarr_table.filter_box_offset = filter.offset.load(Ordering::SeqCst); + } } + + managarr_table.content = content; + managarr_table } pub fn headers(mut self, headers: I) -> Self @@ -72,41 +115,6 @@ where self } - pub fn footer(mut self, footer: Option) -> Self { - self.footer = footer; - self - } - - pub fn footer_alignment(mut self, alignment: Alignment) -> Self { - self.footer_alignment = alignment; - self - } - - pub fn block(mut self, block: Block<'a>) -> Self { - self.block = block; - self - } - - pub fn margin(mut self, margin: u16) -> Self { - self.margin = margin; - self - } - - pub fn loading(mut self, is_loading: bool) -> Self { - self.is_loading = is_loading; - self - } - - pub fn highlight_rows(mut self, highlight_rows: bool) -> Self { - self.highlight_rows = highlight_rows; - self - } - - pub fn sorting(mut self, is_sorting: bool) -> Self { - self.is_sorting = is_sorting; - self - } - fn render_table(self, area: Rect, buf: &mut Buffer) { let table_headers = self.parse_headers(); let table_area = if let Some(ref footer) = self.footer { @@ -160,6 +168,34 @@ where .dimensions(20, 50) .render(table_area, buf); } + + if self.is_searching { + let box_content = &content.search.as_ref().unwrap(); + InputBoxPopup::new(&box_content.text) + .offset(box_content.offset.load(Ordering::SeqCst)) + .block(title_block_centered("Search")) + .render_ref(table_area, buf); + } + + if self.is_filtering { + let box_content = &content.filter.as_ref().unwrap(); + InputBoxPopup::new(&box_content.text) + .offset(box_content.offset.load(Ordering::SeqCst)) + .block(title_block_centered("Filter")) + .render_ref(table_area, buf); + } + + if self.search_produced_empty_results { + Popup::new(Message::new("No items found matching search")) + .size(Size::Message) + .render(table_area, buf); + } + + if self.filter_produced_empty_results { + Popup::new(Message::new("The given filter produced empty results")) + .size(Size::Message) + .render(table_area, buf); + } } else { loading_block.render(table_area, buf); } @@ -189,6 +225,36 @@ where .map(Text::from) .collect() } + + pub fn show_cursor(&self, f: &mut Frame<'_>, area: Rect) { + let mut draw_cursor = |length: usize, offset: usize| { + let table_area = if self.footer.is_some() { + let [content_area, _] = Layout::vertical([Constraint::Fill(0), Constraint::Length(2)]) + .margin(self.margin) + .areas(area); + content_area + } else { + area + }; + let popup_area = Rect { + height: 7, + ..centered_rect(30, 20, table_area) + }; + let [text_box_area, _] = Layout::vertical([Constraint::Length(3), Constraint::Length(1)]) + .margin(1) + .areas(popup_area); + f.set_cursor_position(Position { + x: text_box_area.x + (length - offset) as u16 + 1, + y: text_box_area.y + 1, + }); + }; + + if self.is_searching { + draw_cursor(self.search_box_content_length, self.search_box_offset); + } else if self.is_filtering { + draw_cursor(self.filter_box_content_length, self.filter_box_offset); + } + } } impl<'a, T, F> Widget for ManagarrTable<'a, T, F> diff --git a/src/ui/widgets/managarr_table_tests.rs b/src/ui/widgets/managarr_table_tests.rs index 16c9951..831cd57 100644 --- a/src/ui/widgets/managarr_table_tests.rs +++ b/src/ui/widgets/managarr_table_tests.rs @@ -2,13 +2,13 @@ mod tests { use crate::models::stateful_list::StatefulList; use crate::models::stateful_table::{SortOption, StatefulTable}; - use crate::models::Scrollable; - use crate::ui::utils::layout_block; + use crate::models::{HorizontallyScrollableText, Scrollable}; use crate::ui::widgets::managarr_table::ManagarrTable; use pretty_assertions::assert_eq; use ratatui::layout::{Alignment, Constraint}; use ratatui::text::Text; use ratatui::widgets::{Block, Cell, Row}; + use std::sync::atomic::AtomicUsize; #[test] fn test_managarr_table_new() { @@ -31,6 +31,86 @@ mod tests { assert!(!managarr_table.is_loading); assert!(managarr_table.highlight_rows); assert!(!managarr_table.is_sorting); + assert!(!managarr_table.is_searching); + assert!(!managarr_table.search_produced_empty_results); + assert!(!managarr_table.is_filtering); + assert!(!managarr_table.filter_produced_empty_results); + assert_eq!(managarr_table.search_box_content_length, 0); + assert_eq!(managarr_table.search_box_offset, 0); + assert_eq!(managarr_table.filter_box_content_length, 0); + assert_eq!(managarr_table.filter_box_offset, 0); + } + + #[test] + fn test_managarr_table_new_search_box_populated() { + let items = vec!["item1", "item2", "item3"]; + let mut stateful_table = StatefulTable::default(); + stateful_table.set_items(items.clone()); + let horizontally_scrollable_test = HorizontallyScrollableText { + text: "test".to_owned(), + offset: AtomicUsize::new(3), + }; + stateful_table.search = Some(horizontally_scrollable_test); + + let managarr_table = + ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])); + + let row_mapper = managarr_table.row_mapper; + assert_eq!(managarr_table.content.unwrap().items, items); + assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); + assert_eq!(managarr_table.table_headers, Vec::::new()); + assert_eq!(managarr_table.constraints, Vec::new()); + assert_eq!(managarr_table.footer, None); + assert_eq!(managarr_table.footer_alignment, Alignment::Left); + assert_eq!(managarr_table.block, Block::new()); + assert_eq!(managarr_table.margin, 0); + assert!(!managarr_table.is_loading); + assert!(managarr_table.highlight_rows); + assert!(!managarr_table.is_sorting); + assert!(!managarr_table.is_searching); + assert!(!managarr_table.search_produced_empty_results); + assert!(!managarr_table.is_filtering); + assert!(!managarr_table.filter_produced_empty_results); + assert_eq!(managarr_table.search_box_content_length, 4); + assert_eq!(managarr_table.search_box_offset, 3); + assert_eq!(managarr_table.filter_box_content_length, 0); + assert_eq!(managarr_table.filter_box_offset, 0); + } + + #[test] + fn test_managarr_table_new_filter_box_populated() { + let items = vec!["item1", "item2", "item3"]; + let mut stateful_table = StatefulTable::default(); + stateful_table.set_items(items.clone()); + let horizontally_scrollable_test = HorizontallyScrollableText { + text: "test".to_owned(), + offset: AtomicUsize::new(3), + }; + stateful_table.filter = Some(horizontally_scrollable_test); + + let managarr_table = + ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])); + + let row_mapper = managarr_table.row_mapper; + assert_eq!(managarr_table.content.unwrap().items, items); + assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); + assert_eq!(managarr_table.table_headers, Vec::::new()); + assert_eq!(managarr_table.constraints, Vec::new()); + assert_eq!(managarr_table.footer, None); + assert_eq!(managarr_table.footer_alignment, Alignment::Left); + assert_eq!(managarr_table.block, Block::new()); + assert_eq!(managarr_table.margin, 0); + assert!(!managarr_table.is_loading); + assert!(managarr_table.highlight_rows); + assert!(!managarr_table.is_sorting); + assert!(!managarr_table.is_searching); + assert!(!managarr_table.search_produced_empty_results); + assert!(!managarr_table.is_filtering); + assert!(!managarr_table.filter_produced_empty_results); + assert_eq!(managarr_table.search_box_content_length, 0); + assert_eq!(managarr_table.search_box_offset, 0); + assert_eq!(managarr_table.filter_box_content_length, 4); + assert_eq!(managarr_table.filter_box_offset, 3); } #[test] @@ -56,6 +136,14 @@ mod tests { assert!(!managarr_table.is_loading); assert!(managarr_table.highlight_rows); assert!(!managarr_table.is_sorting); + assert!(!managarr_table.is_searching); + assert!(!managarr_table.search_produced_empty_results); + assert!(!managarr_table.is_filtering); + assert!(!managarr_table.filter_produced_empty_results); + assert_eq!(managarr_table.search_box_content_length, 0); + assert_eq!(managarr_table.search_box_offset, 0); + assert_eq!(managarr_table.filter_box_content_length, 0); + assert_eq!(managarr_table.filter_box_offset, 0); } #[test] @@ -81,174 +169,14 @@ mod tests { assert!(!managarr_table.is_loading); assert!(managarr_table.highlight_rows); assert!(!managarr_table.is_sorting); - } - - #[test] - fn test_managarr_table_footer() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - let footer = "footer".to_owned(); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .footer(Some(footer.clone())); - - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.footer, Some(footer)); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - } - - #[test] - fn test_managarr_table_footer_alignment() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .footer_alignment(Alignment::Center); - - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.footer_alignment, Alignment::Center); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - } - - #[test] - fn test_managarr_table_block() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .block(layout_block()); - - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.block, layout_block()); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - } - - #[test] - fn test_managarr_table_margin() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])).margin(1); - - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.margin, 1); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - } - - #[test] - fn test_managarr_table_loading() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .loading(true); - - let row_mapper = managarr_table.row_mapper; - assert!(managarr_table.is_loading); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - } - - #[test] - fn test_managarr_table_highlight_rows() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .highlight_rows(false); - - let row_mapper = managarr_table.row_mapper; - assert!(!managarr_table.highlight_rows); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(!managarr_table.is_sorting); - } - - #[test] - fn test_managarr_table_sorting() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .sorting(true); - - let row_mapper = managarr_table.row_mapper; - assert!(managarr_table.is_sorting); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); + assert!(!managarr_table.is_searching); + assert!(!managarr_table.search_produced_empty_results); + assert!(!managarr_table.is_filtering); + assert!(!managarr_table.filter_produced_empty_results); + assert_eq!(managarr_table.search_box_content_length, 0); + assert_eq!(managarr_table.search_box_offset, 0); + assert_eq!(managarr_table.filter_box_content_length, 0); + assert_eq!(managarr_table.filter_box_offset, 0); } #[test] diff --git a/src/ui/widgets/message.rs b/src/ui/widgets/message.rs index 7543430..3cfc8f6 100644 --- a/src/ui/widgets/message.rs +++ b/src/ui/widgets/message.rs @@ -1,5 +1,6 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Alignment, Rect}; use ratatui::style::{Style, Stylize}; @@ -10,6 +11,7 @@ use ratatui::widgets::{Paragraph, Widget, Wrap}; #[path = "message_tests.rs"] mod message_tests; +#[derive(Setters)] pub struct Message<'a> { text: Text<'a>, title: &'a str, @@ -30,21 +32,6 @@ impl<'a> Message<'a> { } } - pub fn title(mut self, title: &'a str) -> Self { - self.title = title; - self - } - - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } - - pub fn alignment(mut self, alignment: Alignment) -> Self { - self.alignment = alignment; - self - } - fn render_message(self, area: Rect, buf: &mut Buffer) { Paragraph::new(self.text) .style(self.style) diff --git a/src/ui/widgets/message_tests.rs b/src/ui/widgets/message_tests.rs index cf72796..f15db96 100644 --- a/src/ui/widgets/message_tests.rs +++ b/src/ui/widgets/message_tests.rs @@ -18,42 +18,4 @@ mod tests { assert_eq!(message.style, Style::new().failure().bold()); assert_eq!(message.alignment, Alignment::Center); } - - #[test] - fn test_message_title() { - let test_message = "This is a message"; - let title = "Success"; - - let message = Message::new(test_message).title(title); - - assert_str_eq!(message.title, title); - assert_eq!(message.text, Text::from(test_message)); - assert_eq!(message.style, Style::new().failure().bold()); - assert_eq!(message.alignment, Alignment::Center); - } - - #[test] - fn test_message_style() { - let test_message = "This is a message"; - let style = Style::new().success().bold(); - - let message = Message::new(test_message).style(style); - - assert_eq!(message.style, style); - assert_eq!(message.text, Text::from(test_message)); - assert_str_eq!(message.title, "Error"); - assert_eq!(message.alignment, Alignment::Center); - } - - #[test] - fn test_message_alignment() { - let test_message = "This is a message"; - - let message = Message::new(test_message).alignment(Alignment::Left); - - assert_eq!(message.alignment, Alignment::Left); - assert_eq!(message.text, Text::from(test_message)); - assert_str_eq!(message.title, "Error"); - assert_eq!(message.style, Style::new().failure().bold()); - } } diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index 8b279a5..e27fa2b 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -2,6 +2,7 @@ pub(super) mod button; pub(super) mod checkbox; pub(super) mod confirmation_prompt; pub(super) mod input_box; +mod input_box_popup; pub(super) mod loading_block; pub(super) mod managarr_table; pub(super) mod message; diff --git a/src/ui/widgets/popup.rs b/src/ui/widgets/popup.rs index 0716918..6d63126 100644 --- a/src/ui/widgets/popup.rs +++ b/src/ui/widgets/popup.rs @@ -11,8 +11,9 @@ mod popup_tests; pub enum Size { SmallPrompt, - Prompt, + MediumPrompt, LargePrompt, + WideLargePrompt, Message, NarrowMessage, LargeMessage, @@ -21,14 +22,18 @@ pub enum Size { Small, Medium, Large, + XLarge, + XXLarge, + Long, } impl Size { pub fn to_percent(&self) -> (u16, u16) { match self { Size::SmallPrompt => (20, 20), - Size::Prompt => (37, 37), - Size::LargePrompt => (70, 45), + Size::MediumPrompt => (37, 37), + Size::LargePrompt => (45, 45), + Size::WideLargePrompt => (70, 50), Size::Message => (25, 8), Size::NarrowMessage => (50, 20), Size::LargeMessage => (25, 25), @@ -37,6 +42,9 @@ impl Size { Size::Small => (40, 40), Size::Medium => (60, 60), Size::Large => (75, 75), + Size::XLarge => (83, 83), + Size::XXLarge => (90, 90), + Size::Long => (65, 75), } } } @@ -84,7 +92,16 @@ impl<'a, T: Widget> Popup<'a, T> { } fn render_popup(self, area: Rect, buf: &mut Buffer) { - let popup_area = centered_rect(self.percent_x, self.percent_y, area); + let mut popup_area = centered_rect(self.percent_x, self.percent_y, area); + let height = if popup_area.height < 3 { + 3 + } else { + popup_area.height + }; + popup_area = Rect { + height, + ..popup_area + }; Clear.render(popup_area, buf); background_block().render(popup_area, buf); diff --git a/src/ui/widgets/popup_tests.rs b/src/ui/widgets/popup_tests.rs index 8b68cde..9f82a44 100644 --- a/src/ui/widgets/popup_tests.rs +++ b/src/ui/widgets/popup_tests.rs @@ -7,8 +7,9 @@ mod tests { #[test] fn test_dimensions_to_percent() { assert_eq!(Size::SmallPrompt.to_percent(), (20, 20)); - assert_eq!(Size::Prompt.to_percent(), (37, 37)); - assert_eq!(Size::LargePrompt.to_percent(), (70, 45)); + assert_eq!(Size::MediumPrompt.to_percent(), (37, 37)); + assert_eq!(Size::LargePrompt.to_percent(), (45, 45)); + assert_eq!(Size::WideLargePrompt.to_percent(), (70, 50)); assert_eq!(Size::Message.to_percent(), (25, 8)); assert_eq!(Size::NarrowMessage.to_percent(), (50, 20)); assert_eq!(Size::LargeMessage.to_percent(), (25, 25)); @@ -17,6 +18,9 @@ mod tests { assert_eq!(Size::Small.to_percent(), (40, 40)); assert_eq!(Size::Medium.to_percent(), (60, 60)); assert_eq!(Size::Large.to_percent(), (75, 75)); + assert_eq!(Size::XLarge.to_percent(), (83, 83)); + assert_eq!(Size::XXLarge.to_percent(), (90, 90)); + assert_eq!(Size::Long.to_percent(), (65, 75)); } #[test] diff --git a/src/utils.rs b/src/utils.rs index b5e98db..741e6e4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -67,6 +67,10 @@ pub fn convert_to_gb(bytes: i64) -> f64 { bytes as f64 / 1024f64.powi(3) } +pub fn convert_f64_to_gb(bytes: f64) -> f64 { + bytes / 1024f64.powi(3) +} + pub fn convert_runtime(runtime: i64) -> (i64, i64) { let hours = runtime / 60; let minutes = runtime % 60; diff --git a/src/utils_tests.rs b/src/utils_tests.rs index 6c74458..7e9a533 100644 --- a/src/utils_tests.rs +++ b/src/utils_tests.rs @@ -2,7 +2,7 @@ mod tests { use pretty_assertions::assert_eq; - use crate::utils::{convert_runtime, convert_to_gb}; + use crate::utils::{convert_f64_to_gb, convert_runtime, convert_to_gb}; #[test] fn test_convert_to_gb() { @@ -10,6 +10,12 @@ mod tests { assert_eq!(convert_to_gb(2662879723), 2.4799999995157123); } + #[test] + fn test_convert_f64_to_gb() { + assert_eq!(convert_f64_to_gb(2147483648f64), 2f64); + assert_eq!(convert_f64_to_gb(2662879723f64), 2.4799999995157123); + } + #[test] fn test_convert_runtime() { let (hours, minutes) = convert_runtime(154);